AWS SAM - Serverless Application Model

AWS

What Problem This Solves

The Serverless IaC Challenge: Deploying serverless applications involves managing Lambda functions, API Gateway routes, DynamoDB tables, IAM roles, and event sources. Using raw CloudFormation for serverless is verbose and error-prone. Organizations need a simpler way to define, test locally, and deploy serverless applications.

What AWS SAM Provides: AWS Serverless Application Model (SAM) is an open-source framework that simplifies serverless development:

  • Simplified syntax: Shorter templates compared to CloudFormation
  • Local testing: Test Lambda functions and APIs locally before deployment
  • Built-in best practices: Automatic IAM policies, API Gateway configurations
  • CLI tools: sam build, sam local, sam deploy for the full development lifecycle
  • AWS integration: Extends CloudFormation (SAM templates are valid CloudFormation)

SAM vs CloudFormation vs CDK

Comparison

Feature SAM CloudFormation AWS CDK
Syntax YAML/JSON (simplified) YAML/JSON (verbose) TypeScript, Python, Java, etc. (programming language)
Learning curve Low Medium High
Serverless focus ✅ Yes (optimized) ⚠️ Generic ⚠️ Generic (but good support)
Local testing ✅ Built-in (sam local) ❌ No ❌ No (requires SAM)
Lines of code 50 lines 200 lines 30 lines
Best for Pure serverless apps Multi-service AWS apps Complex infra with logic

Example: Same API in All Three

SAM Template (50 lines):

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

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.handler
      Runtime: python3.9
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

CloudFormation (200+ lines):

Resources:
  HelloWorldFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: my-deployment-bucket
        S3Key: lambda-code.zip
      Handler: app.handler
      Runtime: python3.9
      Role: !GetAtt LambdaExecutionRole.Arn

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  ApiGateway:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: HelloWorldApi
      ProtocolType: HTTP

  ApiGatewayIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ApiGateway
      IntegrationType: AWS_PROXY
      IntegrationUri: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${HelloWorldFunction}
      PayloadFormatVersion: '2.0'

  ApiGatewayRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ApiGateway
      RouteKey: 'GET /hello'
      Target: !Sub integrations/${ApiGatewayIntegration}

  ApiGatewayStage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId: !Ref ApiGateway
      StageName: prod
      AutoDeploy: true

  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref HelloWorldFunction
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/*

# ... more resources (LogGroup, Alarms, etc.)

AWS CDK (30 lines, TypeScript):

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';

export class HelloWorldStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const helloFunction = new lambda.Function(this, 'HelloWorldFunction', {
      runtime: lambda.Runtime.PYTHON_3_9,
      code: lambda.Code.fromAsset('hello-world'),
      handler: 'app.handler',
    });

    const api = new apigateway.RestApi(this, 'HelloWorldApi');
    const helloResource = api.root.addResource('hello');
    helloResource.addMethod('GET', new apigateway.LambdaIntegration(helloFunction));
  }
}

Decision:

  • Use SAM for: Pure serverless applications (Lambda + API Gateway + DynamoDB + EventBridge)
  • Use CDK for: Complex infrastructure requiring programming logic, multi-service apps
  • Use CloudFormation for: Legacy systems, specific AWS features not yet in SAM/CDK

SAM Template Basics

Minimal SAM Template

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31  # Required for SAM

Description: Simple serverless API

Globals:
  Function:
    Timeout: 30
    MemorySize: 512
    Runtime: python3.9
    Environment:
      Variables:
        ENVIRONMENT: production

Resources:
  # Lambda function with API Gateway trigger
  GetUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/get-user/
      Handler: app.lambda_handler
      Events:
        Api:
          Type: Api
          Properties:
            Path: /users/{id}
            Method: GET

  # DynamoDB table
  UsersTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: user_id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

Outputs:
  ApiUrl:
    Description: API Gateway endpoint URL
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'

Resource Types

1. AWS::Serverless::Function

CreateOrderFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: functions/create-order/
    Handler: app.lambda_handler
    Runtime: python3.9
    Timeout: 60
    MemorySize: 1024
    Environment:
      Variables:
        ORDERS_TABLE: !Ref OrdersTable
    Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref OrdersTable
      - SNSPublishMessagePolicy:
          TopicName: !GetAtt OrderCreatedTopic.TopicName
    Events:
      ApiPost:
        Type: Api
        Properties:
          Path: /orders
          Method: POST
      ScheduledEvent:
        Type: Schedule
        Properties:
          Schedule: cron(0 12 * * ? *)  # Daily at noon UTC
      SQSTrigger:
        Type: SQS
        Properties:
          Queue: !GetAtt OrderQueue.Arn
          BatchSize: 10

2. AWS::Serverless::Api

MyApi:
  Type: AWS::Serverless::Api
  Properties:
    StageName: prod
    Cors:
      AllowOrigin: "'*'"
      AllowHeaders: "'Content-Type,Authorization'"
      AllowMethods: "'GET,POST,PUT,DELETE'"
    Auth:
      DefaultAuthorizer: MyCognitoAuthorizer
      Authorizers:
        MyCognitoAuthorizer:
          UserPoolArn: !GetAtt UserPool.Arn
    AccessLogSetting:
      DestinationArn: !GetAtt ApiAccessLogs.Arn
      Format: '$context.requestId $context.error.message $context.error.messageString'
    MethodSettings:
      - ResourcePath: '/*'
        HttpMethod: '*'
        LoggingLevel: INFO
        DataTraceEnabled: true
        MetricsEnabled: true

3. AWS::Serverless::SimpleTable (DynamoDB)

OrdersTable:
  Type: AWS::Serverless::SimpleTable
  Properties:
    PrimaryKey:
      Name: order_id
      Type: String
    ProvisionedThroughput:
      ReadCapacityUnits: 10
      WriteCapacityUnits: 5
    Tags:
      Department: Sales

4. AWS::Serverless::StateMachine (Step Functions)

OrderProcessingStateMachine:
  Type: AWS::Serverless::StateMachine
  Properties:
    DefinitionUri: statemachines/order-processing.asl.json
    Role: !GetAtt StateMachineRole.Arn
    Events:
      OrderCreated:
        Type: EventBridgeRule
        Properties:
          Pattern:
            source:
              - order.service
            detail-type:
              - OrderCreated

SAM CLI Workflow

1. Initialize Project

# Create new SAM application from template
sam init

# Interactive prompts:
# 1. Choose template: AWS Quick Start Templates
# 2. Choose runtime: python3.9
# 3. Choose template: Hello World Example
# 4. Project name: my-serverless-app

# Project structure created:
# my-serverless-app/
#   ├── hello-world/
#   │   ├── app.py
#   │   └── requirements.txt
#   ├── events/
#   │   └── event.json
#   ├── template.yaml
#   └── README.md

2. Build Application

cd my-serverless-app

# Build Lambda function(s) and dependencies
sam build

# Output:
# Building codeuri: hello-world/ runtime: python3.9 metadata: {} architecture: x86_64 functions: HelloWorldFunction
# Running PythonPipBuilder:ResolveDependencies
# Running PythonPipBuilder:CopySource

# Build artifacts in .aws-sam/build/

What sam build does:

  • Resolves dependencies (pip install for Python, npm install for Node.js)
  • Packages Lambda functions into deployment-ready artifacts
  • Validates SAM template syntax
  • Creates .aws-sam/build/ directory with built artifacts

3. Test Locally

Invoke function locally:

# Invoke function with test event
sam local invoke HelloWorldFunction -e events/event.json

# Output:
# Invoking app.lambda_handler (python3.9)
# Skip pulling image and use local one: public.ecr.aws/sam/emulation-python3.9:rapid-1.53.0-x86_64.
#
# Mounting /path/to/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
# START RequestId: 12345678-1234-1234-1234-123456789012 Version: $LATEST
# END RequestId: 12345678-1234-1234-1234-123456789012
# REPORT RequestId: 12345678-1234-1234-1234-123456789012  Duration: 150.00 ms     Billed Duration: 150 ms Memory Size: 512 MB     Max Memory Used: 50 MB
#
# {"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

Start local API Gateway:

# Start API Gateway locally (http://localhost:3000)
sam local start-api

# Test API
curl http://localhost:3000/hello

# Output:
# {"message": "hello world"}

Generate sample events:

# Generate S3 event
sam local generate-event s3 put > events/s3-event.json

# Generate API Gateway event
sam local generate-event apigateway aws-proxy > events/api-event.json

# Generate SQS event
sam local generate-event sqs receive-message > events/sqs-event.json

4. Deploy Application

Guided deployment (first time):

sam deploy --guided

# Interactive prompts:
# Stack Name [my-serverless-app]: my-serverless-app
# AWS Region [us-east-1]: us-east-1
# Confirm changes before deploy [Y/n]: Y
# Allow SAM CLI IAM role creation [Y/n]: Y
# Disable rollback [y/N]: N
# HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
# Save arguments to configuration file [Y/n]: Y
# SAM configuration file [samconfig.toml]: samconfig.toml
# SAM configuration environment [default]: default

# Deployment proceeds:
# - Creates CloudFormation changeset
# - Shows proposed changes
# - Confirms deployment
# - Uploads artifacts to S3
# - Executes CloudFormation stack

# Output:
# CloudFormation outputs from deployed stack
# -------------------------------------------------------------------------------------------------
# Outputs
# -------------------------------------------------------------------------------------------------
# Key                 HelloWorldFunctionIamRole
# Description         Implicit IAM Role created for Hello World function
# Value               arn:aws:iam::123456789012:role/my-serverless-app-HelloWorldFunctionRole-ABC123

# Key                 HelloWorldApi
# Description         API Gateway endpoint URL for Prod stage
# Value               https://abc123def4.execute-api.us-east-1.amazonaws.com/Prod/hello/

# Key                 HelloWorldFunction
# Description         Hello World Lambda Function ARN
# Value               arn:aws:lambda:us-east-1:123456789012:function:my-serverless-app-HelloWorldFunction-XYZ789
# -------------------------------------------------------------------------------------------------

Subsequent deployments (uses saved config):

sam deploy

# Reads from samconfig.toml, deploys without prompts

5. Monitor and Debug

# Tail CloudWatch logs
sam logs -n HelloWorldFunction --tail

# Tail logs with filter
sam logs -n HelloWorldFunction --tail --filter ERROR

# Sync local changes to AWS (live development)
sam sync --watch

# Watches for file changes and automatically deploys

Real-World Example: E-Commerce API

Project Structure

ecommerce-api/
├── functions/
│   ├── create-order/
│   │   ├── app.py
│   │   └── requirements.txt
│   ├── get-order/
│   │   ├── app.py
│   │   └── requirements.txt
│   └── list-orders/
│       ├── app.py
│       └── requirements.txt
├── layers/
│   └── common-libs/
│       └── python/
│           └── lib/
│               └── utils.py
├── events/
│   ├── create-order-event.json
│   └── get-order-event.json
├── template.yaml
├── samconfig.toml
└── README.md

SAM Template

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

Description: E-Commerce API with SAM

Globals:
  Function:
    Timeout: 30
    MemorySize: 512
    Runtime: python3.9
    Environment:
      Variables:
        ORDERS_TABLE: !Ref OrdersTable
        ENVIRONMENT: !Ref Environment
    Layers:
      - !Ref CommonLibsLayer

Parameters:
  Environment:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - staging
      - production

Resources:
  # Lambda Layer for shared code
  CommonLibsLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: common-libs
      Description: Shared utilities and dependencies
      ContentUri: layers/common-libs/
      CompatibleRuntimes:
        - python3.9
      RetentionPolicy: Retain

  # API Gateway
  ECommerceApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Environment
      Cors:
        AllowOrigin: "'*'"
        AllowHeaders: "'Content-Type,Authorization'"
      Auth:
        ApiKeyRequired: true  # Require API key

  # Lambda: Create Order
  CreateOrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/create-order/
      Handler: app.lambda_handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref OrdersTable
        - SNSPublishMessagePolicy:
            TopicName: !GetAtt OrderCreatedTopic.TopicName
      Events:
        CreateOrderApi:
          Type: Api
          Properties:
            RestApiId: !Ref ECommerceApi
            Path: /orders
            Method: POST

  # Lambda: Get Order
  GetOrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/get-order/
      Handler: app.lambda_handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref OrdersTable
      Events:
        GetOrderApi:
          Type: Api
          Properties:
            RestApiId: !Ref ECommerceApi
            Path: /orders/{orderId}
            Method: GET

  # Lambda: List Orders
  ListOrdersFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/list-orders/
      Handler: app.lambda_handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref OrdersTable
      Events:
        ListOrdersApi:
          Type: Api
          Properties:
            RestApiId: !Ref ECommerceApi
            Path: /orders
            Method: GET

  # DynamoDB Table
  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${Environment}-Orders
      BillingMode: PAY_PER_REQUEST  # On-Demand
      AttributeDefinitions:
        - AttributeName: order_id
          AttributeType: S
        - AttributeName: customer_id
          AttributeType: S
        - AttributeName: created_at
          AttributeType: N
      KeySchema:
        - AttributeName: order_id
          KeyType: HASH
      GlobalSecondaryIndexes:
        - IndexName: customer-index
          KeySchema:
            - AttributeName: customer_id
              KeyType: HASH
            - AttributeName: created_at
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES

  # SNS Topic for order events
  OrderCreatedTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: Order Created Notifications

Outputs:
  ApiUrl:
    Description: API Gateway endpoint URL
    Value: !Sub 'https://${ECommerceApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/'

  OrdersTableName:
    Description: DynamoDB table name
    Value: !Ref OrdersTable

  OrderCreatedTopicArn:
    Description: SNS topic ARN for order events
    Value: !Ref OrderCreatedTopic

Function Code (create-order)

# functions/create-order/app.py

import json
import boto3
import os
import uuid
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
sns = boto3.client('sns')

table = dynamodb.Table(os.environ['ORDERS_TABLE'])
topic_arn = os.environ['ORDER_CREATED_TOPIC_ARN']

def lambda_handler(event, context):
    """Create new order"""

    # Parse request body
    body = json.loads(event['body'])

    # Validate input
    if 'customer_id' not in body or 'items' not in body:
        return {
            'statusCode': 400,
            'body': json.dumps({'error': 'Missing required fields'})
        }

    # Create order
    order_id = str(uuid.uuid4())
    order = {
        'order_id': order_id,
        'customer_id': body['customer_id'],
        'items': body['items'],
        'total': sum(item['price'] * item['quantity'] for item in body['items']),
        'status': 'pending',
        'created_at': int(datetime.utcnow().timestamp())
    }

    # Save to DynamoDB
    table.put_item(Item=order)

    # Publish event to SNS
    sns.publish(
        TopicArn=topic_arn,
        Subject='Order Created',
        Message=json.dumps({'order_id': order_id, 'customer_id': body['customer_id']})
    )

    return {
        'statusCode': 201,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        'body': json.dumps({'order_id': order_id, 'status': 'created'})
    }

Deployment

# Build
sam build

# Test locally
sam local start-api

# Test endpoint
curl -X POST http://localhost:3000/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "cust-123",
    "items": [
      {"product_id": "prod-1", "quantity": 2, "price": 29.99},
      {"product_id": "prod-2", "quantity": 1, "price": 49.99}
    ]
  }'

# Deploy to dev
sam deploy --parameter-overrides Environment=dev

# Deploy to production
sam deploy --parameter-overrides Environment=production

CI/CD Pipeline with SAM

GitHub Actions Workflow

# .github/workflows/deploy.yml

name: Deploy Serverless App

on:
  push:
    branches: [main]

env:
  AWS_REGION: us-east-1

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.9'

      - name: Install SAM CLI
        run: |
          pip install aws-sam-cli

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: $

      - name: SAM Build
        run: sam build

      - name: SAM Deploy
        run: |
          sam deploy \
            --no-confirm-changeset \
            --no-fail-on-empty-changeset \
            --stack-name my-serverless-app \
            --s3-bucket my-sam-deployment-bucket \
            --capabilities CAPABILITY_IAM \
            --region $

AWS CodePipeline

# samconfig.toml with CodePipeline integration

version = 0.1

[default.deploy.parameters]
stack_name = "my-serverless-app"
s3_bucket = "my-sam-deployment-bucket"
s3_prefix = "my-serverless-app"
region = "us-east-1"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=production"
image_repositories = []

[default.pipeline.bootstrap.parameters]
pipeline_user = "arn:aws:iam::123456789012:user/aws-sam-cli-managed-pipeline-user"

Bootstrap pipeline:

# Create pipeline infrastructure
sam pipeline init --bootstrap

# Deploy pipeline
sam pipeline bootstrap \
  --stage production \
  --pipeline-user arn:aws:iam::123456789012:user/aws-sam-cli-managed-pipeline-user

# Result: Creates CodePipeline with Source (GitHub) → Build (CodeBuild) → Deploy (CloudFormation)

SAM Policy Templates

Built-in IAM policies for common use cases:

MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: my-function/
    Handler: app.handler
    Policies:
      # DynamoDB policies
      - DynamoDBCrudPolicy:
          TableName: !Ref MyTable
      - DynamoDBReadPolicy:
          TableName: !Ref MyTable

      # S3 policies
      - S3ReadPolicy:
          BucketName: my-bucket
      - S3WritePolicy:
          BucketName: my-bucket
      - S3CrudPolicy:
          BucketName: my-bucket

      # SQS policies
      - SQSSendMessagePolicy:
          QueueName: !GetAtt MyQueue.QueueName
      - SQSPollerPolicy:
          QueueName: !GetAtt MyQueue.QueueName

      # SNS policies
      - SNSPublishMessagePolicy:
          TopicName: !GetAtt MyTopic.TopicName

      # Secrets Manager
      - SecretsManagerReadWrite:
          SecretArn: !Ref MySecret

      # Step Functions
      - StepFunctionsExecutionPolicy:
          StateMachineName: !GetAtt MyStateMachine.Name

      # Custom inline policy
      - Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: '*'

Comparison: SAM vs CDK

When to Use SAM

Pros:

  • ✅ Simpler syntax for serverless-only applications
  • ✅ Built-in local testing (sam local)
  • ✅ Faster deployment (no synth step)
  • ✅ Lower learning curve (YAML-based)
  • ✅ Policy templates for common IAM patterns

Cons:

  • ❌ Limited to serverless resources (Lambda, API Gateway, DynamoDB, etc.)
  • ❌ No programming logic (can’t use loops, conditionals)
  • ❌ Less flexibility for complex infrastructure

Use SAM for:

  • Pure serverless applications (Lambda + API Gateway + DynamoDB)
  • Teams without programming background (DevOps, SysAdmins)
  • Quick prototypes and proof-of-concepts

When to Use CDK

Pros:

  • ✅ Full programming language (TypeScript, Python, Java, etc.)
  • ✅ Reusable constructs and libraries
  • ✅ Supports all AWS services (not just serverless)
  • ✅ Type safety and IDE autocomplete
  • ✅ Conditional logic and loops

Cons:

  • ❌ Steeper learning curve
  • ❌ Requires programming knowledge
  • ❌ No built-in local testing (must use SAM CLI)
  • ❌ Longer deployment (synth CloudFormation → deploy)

Use CDK for:

  • Complex infrastructure requiring logic
  • Multi-service applications (EC2, RDS, ECS, Lambda)
  • Teams with strong programming skills
  • Reusable infrastructure patterns

Common Pitfalls

Pitfall 1: Not Using sam build Before Deploy

Problem: Deploy fails because dependencies not packaged.

# ❌ BAD: Deploy without building
sam deploy  # Fails: "Unable to upload artifact"

# ✅ GOOD: Build then deploy
sam build
sam deploy

Pitfall 2: Forgetting Transform: AWS::Serverless-2016-10-31

Problem: SAM resources not recognized.

# ❌ BAD: Missing Transform
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  MyFunction:
    Type: AWS::Serverless::Function  # Error: Unrecognized resource type

# ✅ GOOD: Include Transform
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31  # Required for SAM

Resources:
  MyFunction:
    Type: AWS::Serverless::Function  # Works

Pitfall 3: Not Setting IAM Permissions for Functions

Problem: Lambda functions can’t access DynamoDB, S3, etc.

# ❌ BAD: No IAM policies
MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: my-function/
    Handler: app.handler
    # Function tries to access DynamoDB but has no permissions → AccessDeniedException

# ✅ GOOD: Explicit IAM policies
MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: my-function/
    Handler: app.handler
    Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref MyTable

Pitfall 4: Using Wrong Event Source Mapping

Problem: DynamoDB Stream trigger doesn’t work.

# ❌ BAD: Using wrong event type for DynamoDB Streams
MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    Events:
      Stream:
        Type: DynamoDB  # Wrong type
        Properties:
          Stream: !GetAtt MyTable.StreamArn

# ✅ GOOD: Use correct event type
MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    Events:
      Stream:
        Type: DynamoDB
        Properties:
          Stream: !GetAtt MyTable.StreamArn
          StartingPosition: LATEST
          BatchSize: 10
          MaximumRetryAttempts: 3

Key Takeaways

SAM Basics:

  1. SAM simplifies serverless: 50 lines (SAM) vs 200 lines (CloudFormation) for same app
  2. Transform required: Always include Transform: AWS::Serverless-2016-10-31 in template
  3. Extends CloudFormation: SAM templates are valid CloudFormation (can mix both)

SAM CLI Workflow:

  1. sam init: Create new project from templates (Python, Node.js, Java, etc.)
  2. sam build: Package Lambda functions and resolve dependencies
  3. sam local: Test functions and APIs locally (Docker-based emulation)
  4. sam deploy: Deploy to AWS (uploads to S3, creates CloudFormation stack)

SAM Resources:

  1. AWS::Serverless::Function: Lambda with automatic IAM role, event sources
  2. AWS::Serverless::Api: API Gateway with automatic integration to Lambda
  3. AWS::Serverless::SimpleTable: DynamoDB table with simplified syntax
  4. AWS::Serverless::StateMachine: Step Functions state machine

Local Testing:

  1. sam local invoke: Test individual functions with JSON events
  2. sam local start-api: Run API Gateway locally (http://localhost:3000)
  3. sam local generate-event: Generate sample events (S3, SNS, SQS, API Gateway)

IAM Policies:

  1. Policy templates: Use built-in templates (DynamoDBCrudPolicy, S3ReadPolicy, etc.)
  2. Automatic IAM roles: SAM creates execution roles automatically
  3. Least privilege: Only grant permissions function needs

CI/CD:

  1. GitHub Actions: Use aws-sam-cli in workflows for automated deployments
  2. AWS CodePipeline: Use sam pipeline bootstrap to create managed pipeline
  3. Multi-environment: Use --parameter-overrides for dev/staging/prod

SAM vs CDK:

  1. Use SAM for: Pure serverless apps, YAML preference, simple use cases
  2. Use CDK for: Complex infrastructure, programming logic needed, multi-service apps
  3. Can mix both: Use SAM for Lambda, CDK for other resources (via CloudFormation imports)

Best Practices:

  1. Globals section: Define common function properties once (Runtime, MemorySize, Timeout)
  2. Layers for shared code: Use Lambda Layers for common dependencies (utils, SDKs)
  3. Environment variables: Pass resource names via environment variables (not hardcoded)
  4. Outputs for integration: Export API URLs, ARNs for cross-stack references

AWS SAM simplifies serverless development with concise templates, local testing, and built-in best practices. Use SAM for pure serverless applications and CDK for complex multi-service architectures. Both extend CloudFormation and can be mixed in the same project.

Found this guide helpful? Share it with your team:

Share on LinkedIn