Summary

AWS SAM (Serverless Application Model) is a CloudFormation extension for building serverless applications on AWS. It introduces shorthand resource types — most notably AWS::Serverless::Function — that expand into full CloudFormation at deploy time.

SAM shines when your architecture is Lambda-centric: every compute unit is a Lambda function, and every trigger is one of the supported event sources. If your stack matches that pattern, SAM removes a massive amount of boilerplate. If it doesn’t, you’re better off with plain CloudFormation or CDK.

Want to see the full source code? All examples in this article are taken from a real serverless project — a portal that grants GitHub repository access via email magic links. The repository is private, but you can claim read access through the portal itself. Head to d2q9mzu0c0r6it.cloudfront.net at the end of this article. Yes — the portal is itself the SAM application described here.

What This Article Covers That Others Don’t

The official Hello World tutorial walks you through sam initsam buildsam deploy --guidedsam delete with a single Lambda function and a single API Gateway route. Most community tutorials follow the same pattern.

This article goes beyond that:

  • When to use SAM — and when not to. No tutorial defines the boundary. This one does: SAM is the right tool if and only if every trigger maps to a supported event source.
  • A multi-function template. Five functions, two DynamoDB tables, CloudFront, S3, SES — not a single Hello World function.
  • Globals with shared environment variables. Not just Timeout: 3, but !Ref to DynamoDB tables and SSM parameters across all functions.
  • Implicit vs. explicit resources. When SAM’s magic creates things for you, and when you need to break out of it (e.g. defining an HttpApi explicitly so CloudFront can reference it).
  • SAM Policy Templates mixed with custom IAM. DynamoDBCrudPolicy next to inline ses:SendEmail statements in the same function.
  • CloudFront + S3 + API Gateway in one SAM template. SAM handles the Lambda routes; CloudFront routes /api/* to the API and everything else to S3. No tutorial shows this pattern.
  • CI/CD with GitHub Actions and OIDC — not sam pipeline, not samconfig.toml, but a real deployment workflow with matrix strategy and PR comments.

What SAM Actually Does

The Hello World tutorial shows sam init creating a project with one function and one route. What it doesn’t make explicit is how much boilerplate SAM eliminates. A regular CloudFormation template for that same single Lambda behind an API Gateway requires you to define the function, a log group, an execution role, the API Gateway (rest API or HTTP API), an integration, a route, a permission, and a deployment/stage. That’s roughly seven resources for one endpoint.

With SAM, you write this:

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

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: app.handler
      Runtime: python3.14
      Events:
        GetApi:
          Type: HttpApi
          Properties:
            Path: /hello
            Method: GET

The Transform: AWS::Serverless-2016-10-31 line tells CloudFormation to expand SAM’s shorthand types into their full equivalents before processing. The Events block on the function declares what triggers it — SAM creates the API Gateway, the integration, the route, and the invoke permission automatically.


When SAM Makes Sense

SAM is the right tool when your architecture satisfies one rule:

Every compute unit is a Lambda function, and every input to those functions is a supported event source.

The event source types SAM supports as of today are:

Event Source Typical Use Case
HttpApi HTTP API Gateway v2 routes
Api REST API Gateway routes
S3 React to file uploads
SQS Queue processing
SNS Pub/sub notifications
Schedule / ScheduleV2 Cron jobs
DynamoDB Stream processing
Kinesis Real-time data streams
CloudWatchLogs Log processing
EventBridgeRule Event-driven workflows
Cognito Auth triggers
MSK / MQ / SelfManagedKafka Message broker consumption
IoTRule IoT event processing
AlexaSkill Voice interface

If every trigger in your design maps to one of these — use SAM. If you need ECS tasks, Step Functions as primary orchestrators, EC2-based workloads, or complex VPC networking as central pieces, SAM adds no value over plain CloudFormation or CDK.

The key insight: SAM automates the glue between a trigger and a Lambda. Outside of that pattern, you’re writing regular CloudFormation anyway — SAM’s shorthand types just don’t cover it.


A Real Example: An Access Portal

Consider a serverless app that grants visitors read access to a private GitHub repository. A visitor enters their email, receives a magic link via SES, and then provides their GitHub username to get an invitation. The app has five Lambda functions, two DynamoDB tables, an HTTP API, a CloudFront distribution, and an S3 bucket for static files.

Here’s the architecture:

                         CloudFront
                             │
                 ┌───────────┴───────────┐
                 │                       │
           Default: /*             /api/*
                 │                       │
                 ▼                       ▼
            S3 Bucket              HTTP API (API Gateway v2)
         (static pages)                  │
                               ┌────┬────┼────┬────┐
                               ▼    ▼    ▼    ▼    ▼
                             Lambda functions
                               │    │    │    │    │
                               ▼    ▼    ▼    ▼    ▼
                          DynamoDB  SES  SSM  GitHub API

The browser only talks to CloudFront. CloudFront routes static assets (/, /claim/, /revoke/) to S3 and API calls (/api/*) to the HTTP API, which triggers the Lambda functions.

Here’s the registration flow a visitor goes through:

  1. Visitor opens the landing page → CloudFront serves index.html from S3
  2. Visitor enters email → POST /api/register → Lambda stores token in DynamoDB, sends magic link via SES
    • SES sandbox workaround: If SES is still in sandbox mode, it can only send to verified addresses. When send_email fails with MessageRejected, the Lambda calls verify_email_identity instead — AWS sends the visitor a verification email. The visitor confirms it, then tries again. On the second attempt, SES can deliver the magic link.
  3. Visitor receives email, clicks link → CloudFront serves /claim/index.html from S3
  4. Claim page sends POST /api/verify → Lambda validates token against DynamoDB, returns email
  5. Visitor enters GitHub username → POST /api/claim → Lambda deletes token from DynamoDB (single-use), calls GitHub API to send collaborator invitation
  6. Visitor accepts the invitation on GitHub → read access granted

Every endpoint is a Lambda triggered by HttpApi. This is a textbook SAM use case.

The template.yaml

The full template is about 300 lines. The following walk-through shows the key SAM patterns, slightly generalized for clarity.

Globals

The Hello World tutorial’s Globals block sets Timeout: 3 and MemorySize: 128. With multiple functions, Globals becomes more powerful — you share environment variables that all functions need:

Globals:
  Function:
    Runtime: !Ref PythonRuntime
    Environment:
      Variables:
        TABLE_NAME: !Ref TokenTable
        EMAIL_TABLE_NAME: !Ref EmailTable
        FROM_EMAIL: !Ref SenderEmail
        TOKEN_TTL_HOURS: "24"

Every function in the template inherits these settings. No need to repeat Runtime or common environment variables five times. Individual functions can still add their own environment variables (e.g. GITHUB_APP_ID) — SAM merges them with the globals.

A Function With Its Trigger

Here’s the RegisterFunction, which handles POST /api/register:

RegisterFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: lambdas/stdlib/
    Handler: src.register.handler
    Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref TokenTable
      - DynamoDBCrudPolicy:
          TableName: !Ref EmailTable
      - Statement:
          Effect: Allow
          Action:
            - ses:SendEmail
            - ses:SendRawEmail
            - ses:VerifyEmailIdentity
          Resource: "*"
    Events:
      RegisterPostApi:
        Type: HttpApi
        Properties:
          ApiId: !Ref HttpApi
          Path: /api/register
          Method: POST

SAM does the following behind the scenes:

  • Creates an IAM execution role with the declared policies (including SAM’s built-in DynamoDBCrudPolicy — a policy template)
  • Creates the Lambda function
  • Adds the POST /api/register route to the HTTP API
  • Creates the integration between the route and the function
  • Adds a Lambda invoke permission for API Gateway

All five functions follow this same pattern. The Events block is the central SAM concept — it’s what makes the tool valuable. Without it, you’d define all of the above manually.

The HTTP API

The API itself is a one-liner:

HttpApi:
  Type: AWS::Serverless::HttpApi

SAM creates an HTTP API Gateway v2 with a default stage. The routes are added automatically from the Events blocks on the functions.

Normally, you wouldn’t need to define this resource at all — SAM creates an implicit HTTP API when it encounters HttpApi events on functions. The explicit definition here is a workaround: the CloudFront distribution needs to reference the API domain as an origin:

Origins:
  - Id: ApiOrigin
    DomainName: !Sub "${HttpApi}.execute-api.${AWS::Region}.amazonaws.com"

Without a named resource, there’s nothing to !Ref. Once the API is explicitly defined, every function must point its events at it via ApiId: !Ref HttpApi — otherwise SAM would create a second, implicit API alongside it.

Non-Serverless Resources

Not everything in the template is SAM-specific. The S3 bucket, CloudFront distribution, DynamoDB tables, and bucket policy are plain CloudFormation:

WebsiteBucket:
  Type: AWS::S3::Bucket

TokenTable:
  Type: AWS::DynamoDB::Table
  Properties:
    BillingMode: PAY_PER_REQUEST
    # ...

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    # ...

This is important: SAM templates are valid CloudFormation templates. You can freely mix SAM shorthand resources with standard CloudFormation resources. SAM doesn’t replace CloudFormation — it extends it for the Lambda trigger pattern.

SAM Policy Templates Mixed With Custom IAM

The Hello World tutorial uses AWSLambdaBasicExecutionRole. In practice, functions need more nuanced permissions. SAM provides policy templates for common patterns — and you can mix them freely with custom IAM statements in the same Policies block:

Policies:
  - DynamoDBCrudPolicy:
      TableName: !Ref TokenTable
  - DynamoDBCrudPolicy:
      TableName: !Ref EmailTable
  - Statement:
      Effect: Allow
      Action:
        - ses:SendEmail
        - ses:SendRawEmail
      Resource: "*"

DynamoDBCrudPolicy is a SAM policy template — it expands into the correct IAM policy with dynamodb:GetItem, PutItem, DeleteItem, Query, Scan, etc. scoped to the table ARN. The Statement block is a standard IAM inline policy. SAM merges both into the function’s execution role. This combination of SAM shorthand and raw IAM is a pattern you’ll need in any real project, but no tutorial shows it.

Build And Deploy

SAM has its own CLI. For manual deployment, the flow is identical to the Hello World tutorial:

sam build --parallel
sam deploy --guided

sam build packages each function’s code (respecting CodeUri and requirements.txt), and sam deploy uploads the artifacts and runs the CloudFormation deployment. The --guided flag interactively creates a samconfig.toml for future deploys.

But no real project stays on sam deploy --guided. In practice, the deploy pipeline is automated via GitHub Actions with OIDC authentication — no stored AWS secrets:

- run: >
    sam build
    --parallel
    --parameter-overrides
    PythonRuntime="python$(cat .python-version)"

- run: >
    sam deploy
    --stack-name "$STACK_NAME"
    --no-confirm-changeset
    --no-fail-on-empty-changeset
    --s3-bucket "$S3_BUCKET"
    --s3-prefix "$STACK_NAME/$GITHUB_SHA"
    --capabilities CAPABILITY_IAM
    --parameter-overrides
    PythonRuntime="python$(cat .python-version)"

After deploy, the static frontend is synced to S3 and CloudFront’s cache is invalidated — both outside of SAM’s scope, done with plain AWS CLI commands.

This is a key difference to the Hello World tutorial’s sam deploy --guided workflow and to sam pipeline init --bootstrap, which generates opaque CI/CD templates. Here, the pipeline is explicit, readable, and uses GitHub’s native OIDC integration instead of stored credentials.


SAM Is A CloudFormation Extension, Not A Replacement

This is the point most tutorials skip. SAM is deliberately scoped. It does not:

  • Manage non-Lambda compute — no ECS, EKS, Fargate, or EC2 abstractions
  • Orchestrate deployments — no built-in canary/blue-green for non-Lambda resources (it does support gradual Lambda deployments via CodeDeploy)
  • Abstract CloudFront, S3, or DynamoDB — these are defined as regular CloudFormation
  • Replace a general-purpose IaC tool — for complex architectures, CDK or Terraform provide more flexibility

The access portal template illustrates this well: the five AWS::Serverless::Function resources with their Events blocks are where SAM earns its keep. The CloudFront distribution, S3 bucket, DynamoDB tables, and IAM policies around them are all standard CloudFormation. SAM simplifies roughly 40% of the template — the Lambda-trigger part — and stays out of the way for the rest.


Local Development

Lambda functions run inside AWS. That’s the fundamental problem with local development — you’re trying to test a cloud resource on your machine. sam local invoke and sam local start-api run your function code in a Docker container, but they don’t replicate DynamoDB, SES, SSM, or any other service your functions talk to. For a project like this — where every function hits DynamoDB and some call SES or the GitHub API — local execution without those services is mostly useless. You can mock them with LocalStack, but that’s a significant setup effort that still doesn’t guarantee parity with real AWS behavior.

The honest answer: write testable functions where you can, and accept that you can’t always. Pure logic — input validation, token generation, response formatting — can be unit tested locally without AWS. But the moment your function reads from DynamoDB or sends an email via SES, you need real cloud resources to verify it works.

For the parts that need real AWS, the fastest iteration loop I found is the Lambda console editor. AWS provides a web-based VS Code editor for each Lambda function. You edit code directly in the browser and click Deploy — the function updates in seconds, no CloudFormation involved. The editor unfortunately lacks extensions and Git support, so it’s not a replacement for your local IDE.

The workflow that worked for me:

  1. Edit in your local VS Code (where you have Git, linting, and all your tooling)
  2. Copy-paste the changes into the Lambda console editor
  3. Click Deploy and test immediately against real DynamoDB, SES, etc.
  4. Once the function works, commit the local version

This is a workaround, not an elegant solution. It only covers code changes to existing functions. If you need to change IAM permissions, add environment variables, or create new resources, you have to trigger a full sam deploy — there’s no shortcut for infrastructure changes.


Conclusion

Use SAM when:

  • Your application is Lambda-centric
  • Every trigger maps to a supported event source
  • You want minimal boilerplate for the Lambda-API Gateway pattern
  • You’re comfortable with CloudFormation for everything else

Skip SAM when:

  • Your compute is primarily containers or VMs
  • You need abstractions beyond Lambda triggers

See The Full Source

Every example in this article is taken from a real SAM project — a portal that grants read access to its own private GitHub repository. The application you’ve been reading about is itself deployed and running.

To explore the complete template.yaml, all Lambda functions, the static frontend, and the CI/CD pipeline, claim access here:

d2q9mzu0c0r6it.cloudfront.net

Enter your email, follow the magic link, provide your GitHub username — and you’ll receive a collaborator invitation to the repository within seconds. The process you go through is the exact application described in this article.