AWS DevOps

AWS SAM Hands-On: Building & Shipping Serverless Apps Fast

You can build a serverless API in raw CloudFormation, but the first time you try it you write about 120 lines of YAML to stand up one Lambda behind one route: the function, its IAM role, the role’s trust and permission policies, the log group, the API, its stage, the deployment, the integration, the route, and the lambda:InvokeFunction permission that lets the API call the code. Nine resources, hand-wired, for one HTTP endpoint. AWS SAM — the Serverless Application Model — collapses all of that into roughly a dozen lines, because SAM is a CloudFormation transform: a macro named AWS::Serverless-2016-10-31 that expands a handful of short-hand resource types into the full CloudFormation you would otherwise type by hand.

That is the whole idea, and it is worth saying precisely because people misfile SAM as “a different IaC tool.” It is not. A SAM template is a CloudFormation template with one extra Transform: line and permission to use five or six special resource types like AWS::Serverless::Function and AWS::Serverless::HttpApi. When you deploy, AWS runs the transform server-side, the short-hand explodes into ordinary AWS::Lambda::Function, AWS::IAM::Role, AWS::ApiGatewayV2::Api and the rest, and from that point on it is a normal CloudFormation stack — same drift detection, same rollback, same change sets. You get CloudFormation’s guarantees with a fraction of CloudFormation’s typing.

The second half of SAM is the SAM CLI (sam), and it is where the day-to-day speed comes from: sam init scaffolds an app, sam build compiles each function per-runtime (esbuild for TypeScript, pip for Python, containers when you ask), sam local start-api runs the whole API on your laptop inside the real Lambda Docker image so you can curl it before you deploy, sam deploy --guided walks you through your first ship and writes a samconfig.toml so every later deploy is one command, and sam sync — Accelerate mode — gives you a sub-ten-second inner loop by pushing code-only changes straight to the service and skipping the full CloudFormation update. By the end of this article you will have built a Lambda + HttpApi + DynamoDB SimpleTable app, run it locally, shipped it, changed a line and watched sam sync update it in seconds, then torn it down — and you will have a 16-row troubleshooting playbook for when sam build can’t find Docker, the transform rejects your Events shape, or a too-narrow policy template throws AccessDenied in production.

Here is the entire SAM surface on one screen — every moving part, what it is, and the beginner trap attached to it:

Piece What it is You write it as The trap that bites first
Transform The macro that expands shorthand Transform: AWS::Serverless-2016-10-31 Omitting it → CFN rejects AWS::Serverless::* as unknown
AWS::Serverless::Function Lambda + role + logs + event wiring One resource with Events, Policies Wrong Events shape → source never wired
AWS::Serverless::HttpApi / Api API Gateway HTTP / REST front door Implicit from an event, or explicit Editing the implicit API you didn’t declare
AWS::Serverless::SimpleTable A single-key DynamoDB table PrimaryKey + billing Needing a GSI it can’t express
Policy templates Least-priv IAM shortcuts Policies: [DynamoDBCrudPolicy: …] Too narrow → runtime AccessDenied
Globals Defaults applied to every function/API A top-level Globals: block Expecting it to cover resource types it doesn’t
sam build Per-runtime compile into .aws-sam sam build (+ --use-container) Native deps fail without a container build
sam local Run functions/API in Docker locally sam local start-api / invoke No Docker daemon → it can’t start
sam deploy Package to S3 + drive CFN changeset sam deploy --guided Missing CAPABILITY_AUTO_EXPAND
sam sync Fast inner loop, skips full CFN sam sync --watch Using it for prod → drift
samconfig.toml Saved deploy parameters Written by --guided Committing secrets/params by accident

What problem this solves

The pain SAM removes is boilerplate density plus a broken inner loop. Raw CloudFormation is verbose by design — it makes no assumptions — so a serverless app that is conceptually “three functions, one API, one table” becomes several hundred lines in which the interesting five lines (the code path, the permission, the route) are buried under role trust policies and API stage plumbing you copy-paste and never read. Worse, CloudFormation has no local story: to test a change you aws cloudformation deploy and wait two to four minutes for a stack update, every time, for a one-character fix. That feedback loop is where serverless productivity goes to die.

What breaks without SAM is not that things are impossible — it is that they are slow and error-prone in ways that compound. You hand-write an IAM role and give it dynamodb:* on * because scoping it correctly is fiddly, and now you have a least-privilege violation in every service. You wire an S3 trigger and forget the AWS::Lambda::Permission, so the bucket silently never invokes the function. You want to try the handler against a realistic API Gateway event and you have no way to do it except deploy-and-curl. Each of these is a half-hour tax, several times a day. SAM’s Policies templates make the scoped role a one-liner, its Events block wires the trigger and the permission for you, and sam local plus sam local generate-event let you replay a real event shape on your laptop.

Here is exactly what the transform collapses — the raw CloudFormation resources SAM generates for you from each line of shorthand, and roughly how much typing it saves:

You write (SAM shorthand) The transform generates (raw CloudFormation) Hand-written lines saved
AWS::Serverless::Function (no role) AWS::Lambda::Function + AWS::IAM::Role + trust policy + AWS::Logs::LogGroup ~25
Events: { Type: HttpApi } AWS::ApiGatewayV2::Api (implicit) + Integration + Route + Stage + AWS::Lambda::Permission ~40
Events: { Type: Api } AWS::ApiGateway::RestApi + Deployment + Stage + Method + Resource + Permission ~50
Events: { Type: S3 } AWS::Lambda::Permission + the bucket notification config wiring ~15
Policies: [DynamoDBCrudPolicy] A full inline IAM policy with 8 scoped dynamodb:* actions on the table ARN ~20
AWS::Serverless::SimpleTable AWS::DynamoDB::Table with PAY_PER_REQUEST and a single-key schema ~12
Globals: { Function: { Timeout: 10 } } The same property copied onto every function in the file N × 1

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need an AWS account with permission to create IAM roles, Lambda functions, API Gateway APIs and DynamoDB tables (a personal or dev sandbox — never straight into production), the AWS CLI v2 configured (aws configure or aws sso login), the AWS SAM CLI installed (brew install aws-sam-cli, or the MSI/pip installer — check with sam --version, you want ≥ 1.100), Docker running (SAM’s local emulation and container builds need it), and Node.js 20 if you follow the TypeScript lab (or Python 3.12 for the Python variant). Everything here is free-tier-friendly: Lambda’s always-free tier, HTTP API’s first million requests, and DynamoDB on-demand at a few reads/writes cost effectively nothing; the only lingering line items are CloudWatch Logs storage and the tiny S3 objects SAM uploads.

Where this sits: SAM is the fastest on-ramp to Infrastructure as Code for serverless specifically. It assumes you already understand a Lambda function’s anatomy — if the handler signature, execution role and CloudWatch Logs are new, build one by hand first with Your First AWS Lambda Function: Handlers, Triggers, Roles & Logs Hands-On. SAM’s most common front door is API Gateway, so the flavors, authorizers and throttling in Amazon API Gateway Hands-On: REST vs HTTP APIs, Authorizers & Throttling are the layer SAM’s Events shorthand is generating for you. SAM is one way to write IaC on AWS; the general-purpose engine underneath it is CloudFormation, covered in AWS CloudFormation: Your First Stack Hands-On, and the imperative alternative that generates CloudFormation from TypeScript is in AWS CDK with TypeScript: Infrastructure as Code Hands-On — read that one for the SAM-vs-CDK decision from the CDK side.

A quick map of who owns what in a SAM project, so when something misbehaves you look in the right place:

Layer What it owns Where it lives When it’s the culprit
template.yaml The desired infrastructure + shorthand Repo root Wrong property names, bad Events shape
samconfig.toml Saved deploy params (stack, region, caps) Repo root Deploys to the wrong account/region
SAM CLI Build, local run, package, deploy driver Your laptop / CI Docker missing, stale .aws-sam cache
SAM transform Shorthand → CloudFormation expansion AWS (server-side) Transform/AUTO_EXPAND errors
CloudFormation Create/update/rollback the real stack AWS Changeset empty, capability, rollback
The resources Lambda, API GW, DynamoDB at runtime AWS AccessDenied, 5xx, throttles

Core concepts

SAM is a transform, not a separate system

Every SAM template starts with two lines that make it a SAM template rather than a plain CloudFormation template:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Transform is a standard CloudFormation feature — it names a macro that CloudFormation runs against your template before it creates resources. AWS::Serverless-2016-10-31 is a macro AWS hosts for you. When you deploy, CloudFormation sends the template to that macro, the macro finds every AWS::Serverless::* resource and rewrites it into ordinary resources, and CloudFormation then processes the expanded template. Because it is a macro, deploying a SAM template through raw CloudFormation requires the CAPABILITY_AUTO_EXPAND capability (you are telling CloudFormation “yes, I know a macro will change my template”), on top of CAPABILITY_IAM for the roles it generates. The sam CLI passes both for you; this is why people who try to aws cloudformation deploy a SAM template directly hit a capability error.

The practical consequences of “it’s just CloudFormation underneath” are worth internalizing:

Because SAM is a transform… …this is true
The expanded template is normal CloudFormation You get drift detection, rollback, change sets, stack policies
You can mix raw CFN and shorthand freely AWS::S3::Bucket and AWS::Serverless::Function in one file
Intrinsics work everywhere !Ref, !GetAtt, !Sub, !ImportValue, Conditions, Mappings
Deploy needs CAPABILITY_AUTO_EXPAND Because a macro rewrites the template
sam validate can’t catch everything Some errors only surface when the macro runs server-side
Outputs/Exports work as usual Cross-stack references are unchanged

The SAM resource types

There are only a handful of short-hand types. Learn what each one expands into and you can predict its behavior:

SAM resource type Expands into (headline resources) Use it for
AWS::Serverless::Function Lambda::Function + IAM::Role + Logs::LogGroup + event wiring Any Lambda, with its triggers and permissions inline
AWS::Serverless::Api ApiGateway::RestApi + Stage + Deployment A REST API (VTL, API keys, usage plans, private)
AWS::Serverless::HttpApi ApiGatewayV2::Api + Stage A cheaper, faster HTTP API (JWT-native, the modern default)
AWS::Serverless::SimpleTable DynamoDB::Table (single key, on-demand) A quick key-value table with one primary key
AWS::Serverless::StateMachine StepFunctions::StateMachine + role + logging A Step Functions workflow with inline event triggers
AWS::Serverless::LayerVersion Lambda::LayerVersion (+ retention control) Shared code/deps packaged as a layer
AWS::Serverless::Application A nested stack from the Serverless Application Repository (SAR) Reusing a published app/component by reference

The Globals section

Globals is a top-level block that sets default properties applied to every resource of a supported type in the file, so you write Timeout, Runtime, MemorySize, Tracing and shared env vars once instead of on each function. It only applies to five resource types, and a per-resource property overrides (or, for maps/lists, merges with) the global. The single most common Globals surprise is expecting it to cover a property or a resource type it does not — for example it applies to Function but there is no global for SimpleTable’s PrimaryKey.

Globals supports Common properties you set there Not covered by Globals
Function Runtime, Handler, Timeout, MemorySize, Architectures, Environment, Tracing, Layers, VpcConfig Policies, Events, FunctionName
Api Cors, Auth, EndpointConfiguration, AccessLogSetting, TracingEnabled Per-method settings, DefinitionBody
HttpApi Auth, CorsConfiguration, AccessLogSettings, DefaultRouteSettings Per-route overrides
SimpleTable SSESpecification PrimaryKey, TableName
StateMachine (limited) Definition, Policies

The SAM template, section by section

A SAM template has the same top-level sections as any CloudFormation template — Transform and Globals are the only SAM-specific additions. Master these and you can read any SAM file:

Section Required? What it holds SAM note
AWSTemplateFormatVersion Optional Always '2010-09-09' Unchanged
Transform Required for SAM AWS::Serverless-2016-10-31 This is what makes it SAM
Description Optional Human description of the stack Unchanged
Globals Optional Fleet-wide defaults SAM-only
Parameters Optional Inputs (--parameter-overrides) Unchanged
Mappings Optional Static lookup tables (!FindInMap) Unchanged
Conditions Optional Boolean gates for resources Unchanged
Resources Required Shorthand + raw resources Mix freely
Outputs Optional Exported values (API URL, ARNs) Unchanged
Metadata Optional Build hints (BuildMethod: esbuild) SAM reads build metadata here

The intrinsic functions you will actually reach for, since a SAM template is CloudFormation:

Intrinsic Does Typical SAM use
!Ref X Physical ID / value of X !Ref ItemsTable → the table name
!GetAtt X.Attr An attribute of X !GetAtt ItemsTable.Arn
!Sub "…${X}…" String interpolation Build the API URL in Outputs
!ImportValue A value exported by another stack Reference a shared VPC/subnet
!FindInMap Look up in Mappings Per-env memory sizes
!If / Conditions Conditional resource/property Create a resource only in prod
!Ref ServerlessHttpApi The implicit HttpApi’s ID Reference an API you didn’t declare

That last row is a genuine gotcha and worth flagging early: when a function declares an HttpApi (or Api) event without pointing at an explicit API resource, SAM creates one implicitly with the logical ID ServerlessHttpApi (or ServerlessRestApi for REST). You reference it by that name in Outputs and !Ref. People spend twenty minutes looking for an API resource they never wrote — it is the implicit one the transform generated.

AWS::Serverless::Function in depth

This is the resource you will write most, so know every property. A function is your code plus its configuration plus its triggers plus its permissions, all in one block.

Property What it sets Values / default When you change it
Runtime Language runtime nodejs20.x, python3.12, java21, go (via provided.al2023), dotnet8, ruby3.3 Match your code; prefer newest
Handler Entry point file.function (e.g. app.handler) Must match your file/export
CodeUri Where the code is Path (., src/) or S3 URI Set to the buildable source dir
MemorySize RAM (and proportional CPU) 128–10240 MB; default 128 Raise for CPU-bound work
Timeout Max seconds per invoke 1–900; default 3 Raise past real p99 latency
Architectures CPU architecture [x86_64] or [arm64] (Graviton) arm64 is cheaper/faster
Environment.Variables Runtime config Key/value map Table names, log level
Policies The role’s permissions Templates, ARNs, inline statements Scope to what the code touches
Events Triggers Map of named event sources Wire API/S3/SQS/schedule
Layers Attached layers List of ARNs / !Ref Share deps/code
Tracing X-Ray mode Active, PassThrough Active for traces
ReservedConcurrentExecutions Concurrency cap Integer Protect a downstream
ProvisionedConcurrencyConfig Pre-warmed instances Integer Kill cold starts on hot paths
EphemeralStorage.Size /tmp size 512–10240 MB; default 512 Big temp files
DeadLetterQueue Async failure sink SQS/SNS target Catch failed async invokes
FunctionUrlConfig Direct HTTPS URL AuthType, Cors Skip API GW for a simple endpoint
AutoPublishAlias Publish + alias on deploy Alias name Enable safe deploys / canaries
DeploymentPreference Canary/linear rollout Type, Alarms, Hooks CodeDeploy-driven gradual shift

Event sources — the Events block

Events is where SAM earns its keep: each entry both configures the trigger and generates the AWS::Lambda::Permission (or event-source mapping) that lets it invoke your function. The Type determines the shape of Properties. Get the shape wrong and the source silently never fires — the number-one “it works in the console but not from the trigger” bug.

Events Type Wires which trigger Key Properties Invocation model
HttpApi API Gateway HTTP API Path, Method, ApiId, Auth Sync
Api API Gateway REST API Path, Method, RestApiId, Auth Sync
S3 S3 bucket notification Bucket, Events, Filter Async
SQS SQS queue poller Queue, BatchSize, ScalingConfig Poll (event-source mapping)
SNS SNS topic subscription Topic, FilterPolicy Async
DynamoDB DynamoDB Streams Stream, StartingPosition, BatchSize Poll
Kinesis Kinesis Data Stream Stream, StartingPosition Poll
Schedule EventBridge Scheduler rule Schedule, Input Async (cron/rate)
ScheduleV2 EventBridge Scheduler (newer) ScheduleExpression, FlexibleTimeWindow Async
EventBridgeRule EventBridge pattern rule Pattern, EventBusName Async
CloudWatchLogs Log subscription filter LogGroupName, FilterPattern Async
Cognito Cognito user-pool trigger UserPool, Trigger Sync
AlexaSkill Alexa skill SkillId Sync
IoTRule IoT topic rule Sql Async
MSK / SelfManagedKafka Kafka poller Stream/KafkaBootstrapServers, Topics Poll
ALB Application Load Balancer target LoadBalancerArn, Path, Method Sync

Policies — least privilege the easy way

The Policies property accepts four different forms, and you can mix them in one list. This flexibility is the whole point: reach for a policy template for the common case, drop to a raw inline statement when the template does not fit.

Form Example When to use
Policy template - DynamoDBCrudPolicy: { TableName: !Ref ItemsTable } Common scoped grants — the default choice
Managed policy ARN - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess A broad AWS-managed policy fits
Inline IAM statement A full Statement list under - Statement: The template doesn’t cover your action
!Ref a policy resource - !Ref MyCustomManagedPolicy Reuse a policy defined elsewhere

Policy templates are named, parameterized, least-privilege snippets. There are around a hundred; these are the ones you will use constantly:

Policy template Grants (scoped) Parameter
DynamoDBCrudPolicy Get/Put/Update/Delete/Query/Scan/BatchGet/BatchWrite on a table TableName
DynamoDBReadPolicy Get/Query/Scan/BatchGet (read-only) TableName
DynamoDBWritePolicy Put/Update/Delete/BatchWrite TableName
DynamoDBStreamReadPolicy Read a table’s stream TableName, StreamName
S3CrudPolicy Get/Put/Delete objects in a bucket BucketName
S3ReadPolicy Get/List objects in a bucket BucketName
SQSSendMessagePolicy sqs:SendMessage* to a queue QueueName
SQSPollerPolicy Receive/Delete/GetAttributes (for pollers) QueueName
SNSPublishMessagePolicy sns:Publish to a topic TopicName
LambdaInvokePolicy lambda:InvokeFunction on a function FunctionName
KMSDecryptPolicy kms:Decrypt on a key KeyId
SSMParameterReadPolicy Read one SSM parameter path ParameterName
VPCAccessPolicy ENI create/describe/delete (Lambda-in-VPC) (none)
StepFunctionsExecutionPolicy states:StartExecution on a state machine StateMachineName
CloudWatchPutMetricPolicy cloudwatch:PutMetricData (none)

The failure mode to burn into memory: a policy template scoped too narrow (say DynamoDBReadPolicy on a function that also writes) passes sam validate, deploys clean, works in every read test, then throws AccessDeniedException: User is not authorized to perform: dynamodb:PutItem the first time it writes in production. The template is doing its job — least privilege — you just picked the wrong one. The fix is the Crud variant or an added inline statement, never dynamodb:* on *.

Build metadata

For compiled/bundled runtimes, sam build reads the function’s Metadata block to know how to build it. For TypeScript this is where you turn on esbuild:

Metadata key Value Effect
BuildMethod: esbuild (nodejs runtimes) Bundle + transpile TS with esbuild
BuildMethod: makefile (any) Run your Makefile target build-<LogicalId>
BuildMethod: nodejs20.x (matches runtime) Default npm-based build
BuildProperties.EntryPoints [src/app.ts] esbuild entry file(s)
BuildProperties.Minify true/false Minify the bundle
BuildProperties.Target es2022 JS target
BuildProperties.Sourcemap true Emit sourcemaps for stack traces

The other resource types

Serverless::Api vs Serverless::HttpApi

Both put an API in front of your functions, but they expand into different services (ApiGateway::RestApi vs ApiGatewayV2::Api) with different features and costs. Pick HttpApi unless you need a REST-only feature.

Dimension Serverless::Api (REST) Serverless::HttpApi (HTTP)
Underlying service API Gateway v1 (REST) API Gateway v2 (HTTP)
Relative cost ~3.5× per request Cheaper (baseline)
Latency Higher Lower
Auth built in IAM, Cognito, Lambda authorizer JWT, IAM, Lambda authorizer
Request/response transform VTL mapping templates None (proxy only)
API keys + usage plans Yes No
Caching Yes No
Private (VPC) endpoints Yes No
WAF integration Yes No (use CloudFront/edge)
CORS Per-method config Simple CorsConfiguration
Implicit logical ID ServerlessRestApi ServerlessHttpApi
Define routes via Events or DefinitionBody (OpenAPI) Events or DefinitionBody

SimpleTable

SimpleTable is deliberately minimal: a DynamoDB table with exactly one primary key and on-demand billing. It is perfect for a lookup or a first table and a trap the moment you need a second access pattern.

SimpleTable property What it sets Note
PrimaryKey.Name Partition key attribute name e.g. id
PrimaryKey.Type String / Number / Binary Only a partition key — no sort key
ProvisionedThroughput RCU/WCU Omit → PAY_PER_REQUEST (on-demand)
SSESpecification Encryption at rest KMS options
TableName Explicit name Omit → CFN-generated name
Tags Cost/owner tags Map
No GlobalSecondaryIndexes Need a GSI/sort key → use AWS::DynamoDB::Table

The moment you need a sort key, a GSI, streams, or TTL, graduate to the raw AWS::DynamoDB::Table — you can keep it in the same SAM file and still reference it with !Ref/!GetAtt.

StateMachine

AWS::Serverless::StateMachine wires a Step Functions workflow with the same Events/Policies conveniences as a function.

StateMachine property What it sets Note
Definition Inline Amazon States Language YAML/JSON in the template
DefinitionUri External ASL file Point at statemachine/def.asl.json
DefinitionSubstitutions ${Var} → resource ARNs Inject function ARNs into the ASL
Type STANDARD or EXPRESS Express = high-volume, short, cheaper
Policies Execution role permissions Same templates as functions
Events Triggers (Schedule, Api, EventBridgeRule) Start executions on events
Logging CloudWatch Logs config Log execution history
Tracing X-Ray Trace the workflow
Role Bring your own role Instead of Policies

LayerVersion and Application (SAR)

Resource / property What it does Note
AWS::Serverless::LayerVersion Publishes a Lambda layer Shared deps/code across functions
ContentUri The layer’s content dir Built by sam build
CompatibleRuntimes Which runtimes may attach it e.g. [nodejs20.x]
RetentionPolicy Retain / Delete old versions Retain keeps history
AWS::Serverless::Application Nests a stack from a template or SAR Reuse a published component
Location SAR ApplicationId + SemanticVersion, or a template path Where the app comes from
Parameters Inputs to the nested app Passed through

The SAM CLI workflow

The CLI is the half of SAM you touch every day. Here is the full command surface, then the flags that matter for the three you run most.

Command Does You run it
sam init Scaffold a new app from a template Once per project
sam validate Lint/validate the template Before every build (in CI)
sam build Compile each function into .aws-sam/build After every code/dep change
sam local invoke Run one function once, locally, in Docker Unit-ish testing a handler
sam local start-api Serve the whole API locally on :3000 Front-end/integration testing
sam local start-lambda Emulate the Lambda service endpoint Point an SDK at local Lambda
sam local generate-event Print a realistic sample event Feed local invoke a real shape
sam deploy Package to S3 + drive a CFN changeset Every real deployment
sam sync Fast inner-loop update (Accelerate) Dev iteration only
sam logs Tail CloudWatch Logs for the stack’s functions Debugging after deploy
sam traces Fetch X-Ray traces Latency/error tracing
sam list Show stack resources/endpoints/outputs “What did I deploy?”
sam package Upload artifacts + rewrite template (no deploy) CI split package/deploy
sam publish Publish an app to the SAR Sharing a component
sam pipeline bootstrap/init Scaffold CI/CD resources + pipeline files One-time CI setup
sam delete Delete the stack + artifacts Teardown

sam build

sam build reads each function’s Runtime/Metadata, resolves its dependencies, and writes a self-contained artifact per function under .aws-sam/build, plus a rewritten template.yaml that points CodeUri at those artifacts.

Flag Effect When
(none) Build on the host toolchain Fast, when host matches the runtime
--use-container Build inside the official Lambda Docker image Native deps / reproducible builds
--parallel Build functions concurrently Many functions
--cached Reuse unchanged build artifacts Speed up repeat builds
--build-dir Change output dir Custom pipelines
--manifest Point at a specific requirements.txt/package.json Non-standard layout
--beta-features Enable preview build features When a feature needs it
-t / --template Use a non-default template file Multiple templates

sam local

Everything under sam local runs your function inside the real Lambda runtime Docker image, so a missing Docker daemon is the first failure. start-api reads your Events of type Api/HttpApi and serves them.

Flag / subcommand Effect Note
start-api --port 3000 Serve the API locally Change port if 3000 is taken
invoke <LogicalId> -e event.json One invocation with a payload -e - reads stdin
generate-event <service> <event> Print a sample event Pipe into invoke
--env-vars env.json Override env vars locally Point at a local DynamoDB
--warm-containers EAGER|LAZY Keep containers warm between calls Faster local iteration
--debug-port 5858 Attach a debugger Step through the handler
--docker-network Join a Docker network Reach local DynamoDB/Postgres
--profile / --region AWS creds/region for real downstream When local calls real AWS

sam deploy and samconfig.toml

sam deploy --guided is the first-run wizard: it prompts for stack name, region, capabilities, and whether to save the answers, then writes samconfig.toml so every later sam deploy is parameter-free. Under the hood it uploads artifacts to S3, expands the transform, creates a change set, and executes it.

sam deploy flag samconfig.toml key Purpose
--guided Interactive first deploy; writes the config
--stack-name stack_name CloudFormation stack name
--region region Target region
--capabilities capabilities CAPABILITY_IAM + CAPABILITY_AUTO_EXPAND
--resolve-s3 resolve_s3 = true Use the SAM-managed artifacts bucket
--s3-bucket s3_bucket Explicit artifacts bucket
--parameter-overrides parameter_overrides Values for Parameters
--confirm-changeset confirm_changeset Show the diff and ask before applying
--no-execute-changeset Create the changeset only (review in CI)
--image-repositories image_repositories ECR repos for image-package functions
--config-env ([env.deploy...]) Named environments (dev/prod) in one file

sam sync / Accelerate — the fast inner loop

sam sync is the productivity headline. Instead of a full CloudFormation update (minutes) for every change, sam sync watches your files and, for a code-only change, calls the Lambda service API (UpdateFunctionCode) directly, skipping CloudFormation entirely — a two-to-ten-second update. For a template change it falls back to a real stack update. It is a development tool: because code-only syncs bypass CloudFormation, the deployed function can drift from what a clean sam deploy of the same template would produce, which is exactly why you never point sam sync at production.

sam sync mode What it does Speed Safe for
sam sync --watch Watch files; auto-choose code vs infra sync Fast (code), slow (infra) Dev
sam sync --code Push code only, always skip CFN Fastest Dev inner loop
--code --resource-id Fn Sync just one function Fastest Dev, targeted
sam sync (no flags) One infra+code sync via CFN Slow Dev
(never) on prod Use sam deploy

sam logs and sam traces close the loop: sam logs --stack-name <s> --tail streams every function’s CloudWatch Logs (add --name <LogicalId> to narrow), and sam traces --tail pulls X-Ray traces so you can see the API-GW → Lambda → DynamoDB latency breakdown without leaving the terminal.

Policy templates vs raw IAM — the decision

Both express the function’s permissions; the question is which to reach for. Templates are least-privilege by construction but only cover common grants; raw IAM covers anything but you own the scoping.

If… Use Why
A common grant exists (DynamoDBCrudPolicy, S3ReadPolicy) Policy template Least-privilege, one line, self-documenting
The action isn’t in any template Inline Statement Full control of Action/Resource/Condition
A broad AWS-managed policy genuinely fits Managed ARN Don’t reinvent AmazonSQSReadOnlyAccess
Org policy mandates a specific customer-managed policy !Ref/ARN Reuse the approved policy
You need cross-resource wiring (Fn → Table) Connectors SAM generates the least-priv policy for the link

AWS::Serverless::Connector (and the Connectors shorthand) deserves a mention: instead of hand-picking a policy template, you declare “this function needs to write to this table” and SAM computes the minimal IAM for the link. It is the newest and often cleanest way to express intent:

PutItemFunction:
  Type: AWS::Serverless::Function
  Connectors:
    TableConn:
      Properties:
        Destination:
          Id: ItemsTable
        Permissions:
          - Write

SAM ↔ CloudFormation interop and sam pipeline

Because SAM is a transform, a SAM template is a place you can put any CloudFormation resource. This is the escape hatch that means you never outgrow SAM for the non-serverless bits of a serverless app.

You can… Because… Example
Add raw CFN resources The expanded template is normal CFN AWS::S3::Bucket, AWS::DynamoDB::Table with a GSI
!Ref/!GetAtt between shorthand and raw They coexist in one template Function reads a raw bucket’s name
Use DependsOn, Conditions, Mappings Standard CFN features Order creation, gate by env
Import existing resources CloudFormation import works Adopt a pre-existing table
Nest stacks AWS::CloudFormation::Stack or Serverless::Application Split a large app
Keep secrets in SSM/Secrets Manager Reference by dynamic reference '{{resolve:ssm:/app/key}}'

For CI/CD, sam pipeline bootstraps the plumbing so you are not hand-writing OIDC roles and artifact buckets:

sam pipeline step Creates For
sam pipeline bootstrap Per-stage IAM roles, artifact + pipeline-exec resources Each environment (dev/prod)
sam pipeline init A ready pipeline file for your CI provider GitHub Actions, GitLab, Jenkins, CodePipeline
(generated) --config-env deploys Environment-scoped sam deploy invocations Promote the same artifact across stages

SAM vs CDK vs raw CloudFormation vs Serverless Framework

There is no single winner; there is a right tool per team and workload. All four ultimately produce CloudFormation (except Serverless Framework, which can target multiple clouds). Decide on language, scope and team.

Dimension SAM CDK Raw CloudFormation Serverless Framework
Authoring YAML shorthand TypeScript/Python/etc. code YAML/JSON YAML + plugins
Best for Serverless-first apps Any AWS infra, imperative Full control, any resource Serverless, multi-cloud
Local run sam local (great) sam local via CDK, or third-party None built in serverless-offline plugin
Fast inner loop sam sync cdk watch (hotswap) None sls deploy function
Abstraction Low (thin over CFN) High (constructs, defaults) None Medium
IAM ergonomics Policy templates Grant methods (table.grantWrite) Hand-written Plugins/iamRoleStatements
Multi-account/CI sam pipeline CDK Pipelines Your own Framework/plugins
Non-AWS resources No (AWS only) No (AWS only) No Yes (providers)
Learning curve Low Medium–high Medium (verbose) Low–medium
Vendor/tooling risk AWS-owned AWS-owned AWS-native Third-party (license history)
Drift/rollback CloudFormation CloudFormation CloudFormation CloudFormation
When to pick Lambda + API + table, fast Complex infra as real code Edge cases, no abstraction Existing SLS shop / multi-cloud

The short version: SAM when the app is serverless and you value a great local loop and minimal YAML; CDK when infrastructure is complex enough that loops, functions and typed abstractions in a real language pay off (see the CDK-side view in AWS CDK with TypeScript: Infrastructure as Code Hands-On); raw CloudFormation when you want zero abstraction or you are templating non-serverless estates (covered in AWS CloudFormation: Your First Stack Hands-On); Serverless Framework when you already run it or genuinely need multi-cloud.

Architecture at a glance

The diagram traces both the workflow (left) and the deployed system (right), because in SAM they are the same story: what you author on the left becomes what runs on the right. You write a compact template.yaml with AWS::Serverless::* shorthand, a Globals block and policy templates. The SAM CLI builds each function per-runtime into .aws-sam/build (esbuild for TypeScript here) and can serve the whole API locally in Docker for curl testing. On sam deploy, the AWS::Serverless-2016-10-31 transform expands the shorthand — each function becomes a Lambda + IAM role + log group + the API permission, the HttpApi becomes a real API Gateway v2 API, and the SimpleTable becomes a DynamoDB table — and CloudFormation drives a change set that creates the stack. At runtime a request hits the HttpApi’s $default stage, invokes the arm64 Node.js function, which writes to the on-demand DynamoDB table via the SDK using exactly the permissions the DynamoDBCrudPolicy template scoped for it. Each numbered badge marks a place a first SAM app breaks; the legend narrates symptom, confirm and fix.

SAM workflow and architecture: template.yaml with Serverless shorthand and Globals flows through sam build and sam local, then the SAM transform expands it into CloudFormation which creates an API Gateway HttpApi, an arm64 Node.js Lambda function, and a DynamoDB SimpleTable, with numbered failure badges on the transform, build, local-Docker, capability, policy-template and sam-sync hops

Real-world scenario

Meridian Freight, a mid-size logistics firm, ran a “shipment events” microservice on a hand-written CloudFormation template: three Lambdas (ingest, enrich, notify), a REST API, a DynamoDB table and an SQS queue, spread across 640 lines of YAML that two engineers understood and nobody else touched. Deploys took four minutes; a one-line handler fix meant a four-minute wait to see it live, so developers batched changes, which made every failed deploy a hunt through five commits. Their IAM was the tell-tale sign of CloudFormation fatigue: every function’s role had dynamodb:* and sqs:* on *, because scoping each one by hand was too tedious to bother with — a finding their first security review flagged as high risk.

They migrated to SAM over a sprint. The 640-line template became 180 lines: functions collapsed into AWS::Serverless::Function blocks with Events wiring the API, queue and stream triggers (deleting ~200 lines of AWS::Lambda::Permission and event-source-mapping boilerplate), and the wildcard roles became DynamoDBCrudPolicy and SQSSendMessagePolicy one-liners scoped to the exact table and queue ARNs — the security finding closed itself. The behavioural change that the team felt most, though, was sam sync --watch: the inner loop for a handler tweak went from four minutes to about seven seconds, so engineers stopped batching and started shipping one change at a time, and the “which of these five commits broke it” problem evaporated.

Two things bit them during the migration, and both are instructive. First, a developer moved the notify function’s policy from DynamoDBCrudPolicy to DynamoDBReadPolicy while “tightening” permissions, not realizing notify also wrote a “last notified” timestamp back to the table; it passed every test (all reads) and threw AccessDeniedException on dynamodb:UpdateItem in production during the first real notification — a five-minute fix once they read the CloudWatch line, but a reminder that “least privilege” means right privilege, not less. Second, someone ran sam sync against the staging stack to “quickly test” a change, then a teammate ran a normal sam deploy of a slightly older template branch, and the code-only drift from the sync got overwritten — they adopted the rule “sam sync on personal dev stacks only; staging and prod ship exclusively through the sam pipeline-generated GitHub Actions workflow.” A year on, the service is 180 lines anyone on the team can read, deploys through a reviewed pipeline, and no role has a wildcard on it.

Advantages and disadvantages

Advantages Disadvantages
Drastically less YAML than raw CloudFormation Only really shines for serverless shapes
Real local testing (sam local, Docker) Local emulation isn’t a perfect prod replica
sam sync gives a seconds-fast inner loop sync can drift from a true CFN deploy
Policy templates = least-privilege by default Templates don’t cover every action
It is CloudFormation — drift, rollback, change sets Inherits CloudFormation’s slow full deploys
Mixes freely with raw CFN resources No abstraction/loops like CDK’s real code
AWS-owned, first-party, no license risk AWS-only (no multi-cloud)
Events wires trigger and permission Wrong Events shape fails silently
Globals removes repetition Globals scope surprises (limited types)
Free tooling, huge template ecosystem Macro errors surface only server-side

SAM’s advantages compound for a team building a serverless-first system: the local loop and sam sync change how fast you iterate, the policy templates change your security posture for free, and “it’s just CloudFormation” means you keep every operational guarantee. The disadvantages matter most at the edges — if your estate is mostly VPCs, EC2 fleets and RDS with a little Lambda, the shorthand buys you little and CDK or raw CloudFormation fits better; if you need constructs, loops and typed reuse, CDK’s real-language authoring wins; if you must target more than AWS, only the Serverless Framework does that.

Hands-on lab

You will scaffold a TypeScript app, replace the template with a Lambda + HttpApi + DynamoDB SimpleTable, build it, run the API locally and curl it, deploy it, hit the live URL, then change a line and watch sam sync update it in seconds — and tear it all down. Everything here is free-tier-friendly. A Python variant is noted at the end.

⚠️ Costs money only if you leave it running long enough to matter: nothing here does at test volume (Lambda free tier, HTTP API’s first million requests, DynamoDB on-demand pennies, a few tiny S3 objects). sam delete at the end removes it all.

Step 0 — Prerequisites check

sam --version          # want >= 1.100
aws sts get-caller-identity   # confirms creds + account
docker ps              # must succeed — sam local & container builds need Docker
node --version         # want v20.x for the TS lab

Expected: sam --version prints something like SAM CLI, version 1.135.0, get-caller-identity prints your account/ARN, docker ps prints a (possibly empty) container table without an error.

Step 1 — Scaffold the app

sam init \
  --name sam-items \
  --runtime nodejs20.x \
  --dependency-manager npm \
  --app-template hello-world \
  --package-type Zip
cd sam-items

Expected: SAM generates a project tree (template.yaml, hello-world/, events/, samconfig later). We are going to replace the template and source with our own.

Step 2 — Write the SAM template

Replace template.yaml with this. Note the Transform line, the Globals defaults, the SimpleTable, the two functions with HttpApi events, the esbuild build metadata, the scoped policy templates, and the Outputs referencing the implicit ServerlessHttpApi.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM hands-on  HttpApi + Lambda + DynamoDB SimpleTable

Globals:
  Function:
    Runtime: nodejs20.x
    Architectures: [arm64]
    MemorySize: 512
    Timeout: 10
    Tracing: Active
    Environment:
      Variables:
        TABLE_NAME: !Ref ItemsTable

Resources:
  ItemsTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String
      # No ProvisionedThroughput => PAY_PER_REQUEST (on-demand)

  PutItemFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2022
        Sourcemap: true
        EntryPoints: [src/put-item.ts]
    Properties:
      Handler: put-item.handler
      CodeUri: .
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ItemsTable
      Events:
        PutItem:
          Type: HttpApi
          Properties:
            Path: /items
            Method: post

  GetItemFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        EntryPoints: [src/get-item.ts]
    Properties:
      Handler: get-item.handler
      CodeUri: .
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref ItemsTable
      Events:
        GetItem:
          Type: HttpApi
          Properties:
            Path: /items/{id}
            Method: get

Outputs:
  ApiUrl:
    Description: HttpApi base URL
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"

Step 3 — Write the handlers and package.json

Create src/put-item.ts:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "node:crypto";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME!;

export const handler = async (event: any) => {
  const body = JSON.parse(event.body ?? "{}");
  const id = body.id ?? randomUUID();
  await ddb.send(new PutCommand({ TableName: TABLE, Item: { id, ...body } }));
  return { statusCode: 201, body: JSON.stringify({ id }) };
};

Create src/get-item.ts:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME!;

export const handler = async (event: any) => {
  const id = event.pathParameters?.id;
  const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { id } }));
  if (!res.Item) return { statusCode: 404, body: JSON.stringify({ error: "not found" }) };
  return { statusCode: 200, body: JSON.stringify(res.Item) };
};

Create package.json (esbuild bundles the SDK, so it stays out of the deployed zip if you mark it external — here we keep it simple and bundle):

{
  "name": "sam-items",
  "version": "1.0.0",
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.600.0",
    "@aws-sdk/lib-dynamodb": "^3.600.0"
  },
  "devDependencies": { "esbuild": "^0.23.0" }
}

Then npm install. Delete the scaffolded hello-world/ directory so only your src/ remains.

Step 4 — Validate and build

sam validate --lint
sam build

Expected: validate prints template.yaml is a valid SAM Template; build prints per-function Building codeuri: … Runtime: nodejs20.x … Build method: esbuild, then:

Build Succeeded
Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Step 5 — Run the API locally and curl it

sam local start-api reads your HttpApi events and serves them on :3000 inside Docker (it will pull the Lambda image the first time). For the local write to reach a real table you would point TABLE_NAME at DynamoDB Local; to keep this step pure-local we just confirm the route wiring and handler execution.

sam local start-api          # in one terminal
# in another terminal:
curl -s -XPOST http://127.0.0.1:3000/items -d '{"name":"widget","qty":3}'

Expected: the start-api terminal logs Mounting PutItemFunction at http://127.0.0.1:3000/items [POST] and, on the request, spins a container and prints START/END/REPORT lines; the curl returns {"id":"<uuid>"} (or a DynamoDB credentials/endpoint error if you pointed it at real AWS without creds — that still proves the route and handler ran). Stop the server with Ctrl-C.

Step 6 — Deploy for real (guided)

sam deploy --guided

Answer the prompts: stack name sam-items, your region, Allow SAM CLI IAM role creation = Y (this is the CAPABILITY_IAM consent), Disable rollback = N, allow the functions to have no auth for this demo (HttpApi has no authorization defined, Is this okay? = y), and Save arguments to configuration file = Y. SAM creates the managed S3 bucket, uploads, expands the transform, and drives the change set.

Expected: a change-set preview listing AWS::Lambda::Function, AWS::IAM::Role, AWS::ApiGatewayV2::Api, AWS::DynamoDB::Table and permissions being Added, then Successfully created/updated stack - sam-items and an Outputs table:

Key                 ApiUrl
Description         HttpApi base URL
Value              https://ab12cd34ef.execute-api.ap-south-1.amazonaws.com

A samconfig.toml now exists — every later sam deploy needs no flags.

Step 7 — Hit the live API

API=$(aws cloudformation describe-stacks --stack-name sam-items \
  --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text)

curl -s -XPOST "$API/items" -d '{"name":"widget","qty":3}'    # -> {"id":"<uuid>"}
ID=<paste-the-uuid>
curl -s "$API/items/$ID"                                       # -> {"id":"…","name":"widget","qty":3}

Expected: the POST returns a fresh id; the GET returns the stored item from the real DynamoDB table. You just proved the scoped DynamoDBCrudPolicy/DynamoDBReadPolicy grants work end to end.

Step 8 — The fast inner loop with sam sync

Change put-item.ts to also stamp a timestamp, e.g. add createdAt: new Date().toISOString() into the Item. Then:

sam sync --watch --stack-name sam-items

Expected: on the first run SAM asks to confirm (it warns this is for dev), then on save it prints something like Syncing Function PutItemFunction and Finished syncing … 0m6s — a code-only update via UpdateFunctionCode, no CloudFormation change set. curl the POST again and the response item now carries createdAt. Stop with Ctrl-C.

Step 9 — Logs and traces

sam logs --stack-name sam-items --name PutItemFunction --tail
sam traces --tail        # X-Ray, since Tracing: Active is set globally

Expected: sam logs streams the function’s CloudWatch log events (including your START/REPORT); sam traces shows the API-GW → Lambda → DynamoDB segment timings.

Step 10 — Teardown

sam delete --stack-name sam-items

Expected: SAM asks to confirm, deletes the CloudFormation stack (Lambda, role, API, table, log groups) and the artifacts it uploaded, and prints Deleted successfully. Verify with aws cloudformation describe-stacks --stack-name sam-items returning a does not exist error.

Python variant: sam init --runtime python3.12 --dependency-manager pip, drop the esbuild Metadata (Python builds from requirements.txt), set Handler: app.handler, CodeUri: src/, and use boto3 in app.py. Everything else — Globals, SimpleTable, Events, Policies, the whole CLI loop — is identical.

Common mistakes & troubleshooting

This is the section you will come back to. First the structured playbook, then an error-string reference, then a decision table and prose on the three nastiest.

# Symptom Root cause Confirm (exact command / path) Fix
1 Unresolved resource dependencies / Invalid Serverless Application Specification Missing Transform line, or a bad Events property name sam validate --lint Add Transform: AWS::Serverless-2016-10-31; fix the property against the SAM spec
2 sam build fails: RuntimeError … requires the Docker / native module errors Building native deps on a mismatched host sam build --debug sam build --use-container to build in the Lambda image
3 sam build fails: Build method 'esbuild' … requires esbuild not installed / wrong Metadata npm ls esbuild npm i -D esbuild; verify BuildMethod: esbuild + EntryPoints
4 sam local start-api errors: Cannot connect to the Docker daemon Docker isn’t running docker ps Start Docker Desktop / dockerd; retry
5 sam local errors: port 3000 is in use / address already in use Another process on 3000 lsof -i :3000 sam local start-api --port 3001
6 sam local returns creds/endpoint errors on a DB call Local container has no AWS creds / real endpoint Read the handler error in the terminal --env-vars → DynamoDB Local, or accept it (route/handler still proved)
7 sam deploy fails: Requires capabilities: [CAPABILITY_AUTO_EXPAND] Deploying the macro without expand consent The deploy error text sam deploy (adds it), or --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND
8 sam deploy fails: Requires capabilities: [CAPABILITY_IAM] Stack creates IAM roles The deploy error text Add CAPABILITY_IAM (or answer Y to role creation in --guided)
9 sam deploy: No changes to deploy. Stack … is up to date Change set is empty (nothing changed) sam deploy output Actually change code/template; or --force-upload won’t help — it’s a real no-op
10 sam deploy fails: Unable to upload artifact … bucket does not exist No artifacts bucket configured Check samconfig.toml for s3_bucket/resolve_s3 sam deploy --resolve-s3 (managed bucket) or --s3-bucket <name>
11 API returns 500; logs show AccessDeniedException … dynamodb:PutItem Policy template too narrow (ReadPolicy on a writer) sam logs --name Fn --tail; read the action Use DynamoDBCrudPolicy (or add an inline Statement) scoped to the table
12 Trigger (S3/SQS/schedule) never fires; function is silent Wrong Events Type/shape → source not wired aws lambda get-policy --function-name <fn> (no permission = not wired) Fix the Events block to the correct Type + Properties; redeploy
13 Global default (Timeout/Runtime) not applied to a function Property set in Globals that Globals doesn’t support, or overridden locally Inspect expanded template: sam build then read .aws-sam/build/template.yaml Move it to a supported Globals type, or set it per-resource
14 !Ref ServerlessHttpApi / ServerlessRestApidoes not exist Referencing the implicit API when routes are on an explicit one (or vice-versa) sam list resources --stack-name <s> Reference the actual API’s logical ID; or declare an explicit AWS::Serverless::HttpApi
15 Prod behaves unlike the template after edits sam sync code-drift never reconciled with CloudFormation Compare live code vs sam deploy output Run a full sam deploy; restrict sam sync to dev stacks
16 sam deploy rolls back: ROLLBACK_COMPLETE, can’t update again A failed create left the stack in a dead state aws cloudformation describe-stack-events --stack-name <s> sam delete then redeploy; fix the resource that failed first

Error/status reference — the strings you will actually see and what they mean:

String / code Where Meaning Fix
CAPABILITY_AUTO_EXPAND required sam deploy You’re deploying a macro (SAM) template Let sam add it, or pass it explicitly
CAPABILITY_IAM / CAPABILITY_NAMED_IAM required sam deploy Stack creates roles (named roles need the second) Add the capability / answer Y to role creation
No changes to deploy sam deploy Empty change set Change something real
Runtime … is not supported sam build Typo’d/retired runtime Use a current runtime string
Cannot connect to the Docker daemon sam local/build --use-container Docker not running Start Docker
Error: Running AWS SAM projects locally requires Docker sam local Same Start Docker
AccessDeniedException Runtime (CloudWatch) Role/policy lacks the action Fix the policy template / statement
ResourceNotFoundException Runtime Wrong table/queue name (env var) Check Environment.Variables / !Ref
502 Bad Gateway HttpApi Handler returned a bad shape or threw Return {statusCode, body}; read logs
403 Forbidden (missing auth) HttpApi Route needs auth or wrong path Check Auth/route; base path
Transform … failed Deploy Macro couldn’t expand the template sam validate --lint; fix shorthand
ROLLBACK_COMPLETE CloudFormation Create failed; stack unusable sam delete + redeploy

Decision table — you see X, it’s probably Y, do Z:

If you see… It’s probably… Do this
Works in sam local, 500 in prod Missing/narrow IAM (local uses your creds) Widen the policy template to what the code does
A trigger never fires but console-test works Events shape wrong → no permission generated Fix Events; confirm with aws lambda get-policy
Deploy says “no changes” but you edited code You edited but didn’t sam build, or edited the built copy sam build then sam deploy
Global timeout ignored on one function Local override, or unsupported in Globals Read the expanded template; set per-resource
sam sync fast but prod later “reverts” A full deploy overwrote synced code-drift Only sync dev; ship prod via sam deploy
Capability errors on a raw cloudformation deploy You bypassed sam for a macro template Use sam deploy, or pass CAPABILITY_AUTO_EXPAND

The three nastiest, in prose. First, the too-narrow policy template: it is insidious precisely because everything green-lights it. sam validate passes, the deploy succeeds, every read test works, and it fails only on the first write in production with AccessDeniedException: dynamodb:UpdateItem. The lesson is that “least privilege” is correct privilege — read the code, enumerate every action it performs, and pick the template (or write the inline statement) that grants exactly those. The confirm is always the same: sam logs --name <Fn> --tail and read the exact action the SDK error names.

Second, the silently un-wired event source: you write an Events block with a slightly wrong Type or property (say Queue where SAM expects a !GetAtt Queue.Arn, or S3 without Bucket), the transform accepts it as some valid shape, and no AWS::Lambda::Permission gets generated — so the trigger fires into the void and your function is simply never invoked, with nothing in the logs because it never ran. The confirm is not in the function’s logs (there are none); it is aws lambda get-policy --function-name <fn> — if the source’s permission statement is missing, the wiring is broken. Compare your Events block against the SAM spec property-by-property.

Third, sam sync drift: sam sync --code pushes straight to the Lambda service, skipping CloudFormation, so the deployed function no longer matches what a clean sam deploy of your template would produce. On a personal dev stack that is exactly what you want (speed). The trap is running it against a shared stack: the next real sam deploy — from CI, or a colleague, or you on a different branch — reconciles the stack back to the template and silently discards your synced changes, or worse, someone debugging “prod” is looking at code that exists in no commit. Rule: sam sync on ephemeral personal stacks only; staging and prod ship exclusively through sam deploy in a pipeline.

Best practices

Security notes

SAM’s security story is mostly IAM ergonomics plus CloudFormation’s guarantees, and the controls are the ones that keep a serverless app least-privileged and auditable:

Control How SAM does it Do this
Least-privilege function roles Policy templates scope to a named resource ARN Use Crud/Read/Write variants; avoid wildcards
No shared roles by default Each Function gets its own generated role Don’t force one role across functions
Deploy-time IAM consent CAPABILITY_IAM / CAPABILITY_NAMED_IAM Review the change set’s IAM before approving
Secrets out of the template SSM/Secrets Manager dynamic references '{{resolve:ssm:/app/db}}', never plaintext env
Encryption at rest SimpleTable.SSESpecification, KMS on env vars Enable KMS; grant kms:Decrypt narrowly
API authorization Auth on HttpApi/Api (JWT, IAM, Lambda authz) Don’t ship NONE auth beyond a demo
Network isolation VpcConfig + VPCAccessPolicy Put functions in a VPC to reach private data
Supply-chain integrity --use-container builds, pinned deps Reproducible artifacts; scan dependencies
Pipeline trust sam pipeline OIDC roles, per-stage No long-lived deploy keys in CI
Auditability It’s CloudFormation → CloudTrail on every change Alert on out-of-band stack drift

The single highest-value habit is the policy-template discipline: because SAM makes a scoped role a one-liner, there is no longer any excuse for the wildcard roles that raw-CloudFormation fatigue used to produce. Read each function’s code, grant exactly its actions on exactly its resources, and let the generated per-function role isolate blast radius.

Cost & sizing

SAM itself is free — you pay only for what the stack deploys. The cost drivers are the runtime resources, and the sizing levers are the function properties:

Driver What it costs Lever
Lambda invocations Per request (first 1M/mo free) Fewer, batched invokes; right-size triggers
Lambda GB-seconds MemorySize × duration Tune MemorySize; arm64 is ~20% cheaper
HTTP API requests Per million (first 1M/mo free) Cheaper than REST for the same traffic
REST API requests ~3.5× HTTP API per million Only pay it for REST-only features
DynamoDB (on-demand) Per read/write request unit On-demand for spiky; provisioned for steady
DynamoDB storage Per GB-month TTL to expire old items
CloudWatch Logs Ingestion + storage Set retention; log at the right level
X-Ray traces Per trace recorded Sample; Active only where needed
S3 artifacts Tiny (the deploy zips) Lifecycle-expire old artifacts

Sizing guidance and free-tier reality:

Choice Small/dev Production Note
MemorySize 128–512 MB Right-sized from tuning More memory = more CPU
Architectures arm64 arm64 Cheaper + faster default
API flavor HttpApi HttpApi unless REST-only Cost + latency
DynamoDB billing On-demand On-demand (spiky) / provisioned (steady) On-demand is zero-ops
Log retention 7 days 30–90 days Uncapped logs quietly bill

For the lab in this article, the total cost at test volume rounds to zero — a few dozen invocations, a few DynamoDB requests, a megabyte of logs and a tiny S3 object, all inside the free tier. In INR terms a light dev app that stays within the always-free Lambda and first-million HTTP-API tiers is often ₹0–₹50/month; the bill only grows when real traffic, verbose logging, or an under-sized memory setting (paying for long durations) kicks in. The performance-and-cost dials live in AWS Lambda Memory, Timeout & Concurrency Tuning — SAM just makes them one-line properties.

Interview & exam questions

1. What exactly is AWS SAM? A CloudFormation transform (AWS::Serverless-2016-10-31) plus a CLI. The transform is a macro that expands short-hand AWS::Serverless::* resource types into full CloudFormation at deploy time; a SAM template is therefore a superset of a CloudFormation template. (DVA-C02)

2. Why does deploying a SAM template need CAPABILITY_AUTO_EXPAND? Because SAM is implemented as a macro, and CloudFormation requires CAPABILITY_AUTO_EXPAND to run any macro that rewrites your template — on top of CAPABILITY_IAM for the roles SAM generates. The sam CLI passes both automatically. (DVA-C02)

3. What does AWS::Serverless::Function expand into? At minimum an AWS::Lambda::Function, an AWS::IAM::Role (with a trust policy and your Policies), and a LogGroup; plus, per Events entry, the trigger resources and the AWS::Lambda::Permission/event-source-mapping that wire it. (DVA-C02)

4. When would you choose Serverless::Api over Serverless::HttpApi? When you need a REST-only feature: VTL request/response mapping, API keys and usage plans, response caching, private (VPC) endpoints, or native WAF/edge integration. Otherwise HttpApi is cheaper, faster and the default. (SAA-C03, DVA-C02)

5. What are policy templates and why prefer them? Named, parameterized least-privilege IAM snippets (e.g. DynamoDBCrudPolicy: {TableName: …}) that expand to a scoped inline policy on the function’s role. They make correct least-privilege a one-liner instead of hand-written IAM, reducing the temptation to use wildcards. (SCS-C02, DVA-C02)

6. What’s the difference between sam deploy and sam sync? sam deploy packages to S3, expands the transform and drives a full CloudFormation change set (minutes) — the way you ship. sam sync (Accelerate) is a dev inner loop: for code-only changes it calls the service API directly (UpdateFunctionCode), skipping CloudFormation for a seconds-fast update, at the cost of possible drift — never for prod. (DVA-C02)

7. What does Globals do and what are its limits? It sets default properties applied to every resource of a supported type (Function, Api, HttpApi, SimpleTable, StateMachine). Per-resource properties override globals. It does not cover every property (e.g. not Policies/Events on functions) or every resource type. (DVA-C02)

8. How do you use a raw CloudFormation resource in a SAM template? Just declare it — a SAM template is CloudFormation, so AWS::S3::Bucket or a full AWS::DynamoDB::Table with a GSI coexists with shorthand, and you cross-reference with !Ref/!GetAtt. This is how you handle anything SAM’s shorthand doesn’t express. (DVA-C02, SAP-C02)

9. Your S3-triggered function never runs and has no logs. Diagnose it. No logs means it never invoked; the usual cause is a wrong Events shape so no AWS::Lambda::Permission was generated. Confirm with aws lambda get-policy --function-name <fn> — a missing source permission proves the wiring. Fix the Events block and redeploy. (DVA-C02)

10. SAM vs CDK — how do you choose? SAM for serverless-first apps that value minimal YAML and a great local loop (sam local, sam sync); CDK when infrastructure is complex enough that authoring it as real typed code with constructs, loops and grant* methods pays off. Both compile to CloudFormation. (SAP-C02)

11. What’s a SimpleTable and when do you outgrow it? A DynamoDB table with a single partition key and on-demand billing, expressed in a few lines. You outgrow it the instant you need a sort key, a GSI, streams or TTL — then switch to raw AWS::DynamoDB::Table in the same file. (DVA-C02)

12. How does SAM support CI/CD? sam pipeline bootstrap provisions per-stage IAM roles and artifact resources; sam pipeline init generates a pipeline definition for your CI provider (GitHub Actions, GitLab, Jenkins, CodePipeline). Combined with --config-env, you promote the same artifact across environments through reviewed sam deploys. (DVA-C02, SAP-C02)

Quick check

  1. What single line turns a CloudFormation template into a SAM template, and what capability does deploying it therefore require?
  2. Name the resource type that expands into a Lambda function plus its role, log group and event wiring.
  3. Why might a function pass every test yet throw AccessDeniedException in production, and how do you confirm which action is denied?
  4. What is the key operational difference between sam deploy and sam sync, and why is sam sync unsafe for prod?
  5. A function’s HttpApi event route works, but !Ref ServerlessHttpApi in Outputs errors. What’s going on?

Answers

  1. Transform: AWS::Serverless-2016-10-31; because it is a macro, deploying it requires CAPABILITY_AUTO_EXPAND (plus CAPABILITY_IAM for the generated roles).
  2. AWS::Serverless::Function — it expands into AWS::Lambda::Function + AWS::IAM::Role + LogGroup + per-event trigger resources and AWS::Lambda::Permission.
  3. A policy template scoped too narrow (e.g. DynamoDBReadPolicy on a function that also writes) passes read tests and fails on the first write; confirm with sam logs --name <Fn> --tail and read the exact dynamodb:* action the SDK error names, then use the Crud variant or an inline statement.
  4. sam deploy drives a full CloudFormation change set (the way you ship); sam sync pushes code-only changes directly to the Lambda service, skipping CloudFormation for a seconds-fast dev loop — which causes drift from the template, so it’s dev-only.
  5. You are referencing the wrong logical ID — this errors only if the routes are actually on an explicit API you declared (or none exists); when routes use an implicit API, SAM creates it as ServerlessHttpApi, which is exactly what you reference. Use sam list resources to see the real logical ID.

Glossary

Term Definition
SAM AWS Serverless Application Model — a CloudFormation transform + CLI for building serverless apps.
Transform A CloudFormation macro named in the template; AWS::Serverless-2016-10-31 is SAM’s.
Macro Server-side code CloudFormation runs to rewrite a template before creating resources.
CAPABILITY_AUTO_EXPAND The consent required to deploy a template that a macro will rewrite.
Shorthand resource An AWS::Serverless::* type that expands into multiple raw resources.
Globals Template section setting default properties for supported resource types.
Policy template A named, parameterized least-privilege IAM snippet (e.g. DynamoDBCrudPolicy).
Events A function’s triggers; each entry also generates the invoke permission/mapping.
SimpleTable A single-key, on-demand DynamoDB table expressed in a few lines.
Implicit API The ServerlessHttpApi/ServerlessRestApi SAM creates when an event needs an API you didn’t declare.
sam build Compiles each function per-runtime into .aws-sam/build.
sam local Runs functions/APIs locally inside the Lambda Docker image.
sam sync / Accelerate Fast dev inner loop that pushes code-only changes straight to the service.
samconfig.toml Saved deploy parameters written by sam deploy --guided.
SAR Serverless Application Repository — publish/reuse apps via AWS::Serverless::Application.
Connector AWS::Serverless::Connector — declares intent (Read/Write) and generates least-priv IAM for a link.

Next steps

AWSSAMServerlessCloudFormationLambdaSAM CLIIaCHTTP API
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading