Module Overview
Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Lambda, CloudFormation basics
- SAM template structure and syntax
- Local development and testing
- Deployment and CI/CD integration
- Common serverless patterns
- SAM CLI commands and workflows
Why SAM?
Simplified Syntax
Write less CloudFormation—SAM handles the boilerplate
Local Testing
Test Lambda functions and APIs locally before deployment
Best Practices Built-in
Automatic IAM policies, API Gateway configuration, and more
Portable
Deploy anywhere—SAM transforms to standard CloudFormation
SAM Template Structure
Copy
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: My Serverless Application
# Global configuration for all functions
Globals:
Function:
Runtime: python3.11
Timeout: 30
MemorySize: 256
Tracing: Active
Environment:
Variables:
LOG_LEVEL: INFO
TABLE_NAME: !Ref OrdersTable
Parameters:
Environment:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
Resources:
# Lambda Function
OrderFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
CodeUri: src/
Description: Process orders
Events:
ApiEvent:
Type: Api
Properties:
Path: /orders
Method: POST
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrdersTable
# DynamoDB Table
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub ${Environment}-orders
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: order_id
AttributeType: S
KeySchema:
- AttributeName: order_id
KeyType: HASH
Outputs:
ApiUrl:
Description: API Gateway URL
Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/
SAM Resource Types
AWS::Serverless::Function
Copy
MyFunction:
Type: AWS::Serverless::Function
Properties:
# Required
Handler: index.handler
Runtime: python3.11
CodeUri: ./src
# Optional - Compute
MemorySize: 512
Timeout: 30
Architectures:
- arm64 # Graviton2 for cost savings
# Optional - VPC
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
# Optional - Tracing and Logging
Tracing: Active
LoggingConfig:
LogFormat: JSON
ApplicationLogLevel: INFO
# Optional - Concurrency
ReservedConcurrentExecutions: 100
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 10
# Event Sources
Events:
ApiEvent:
Type: Api
Properties:
Path: /items
Method: GET
ScheduleEvent:
Type: Schedule
Properties:
Schedule: rate(1 hour)
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt MyQueue.Arn
BatchSize: 10
S3Event:
Type: S3
Properties:
Bucket: !Ref MyBucket
Events: s3:ObjectCreated:*
# Permissions
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MyTable
- S3ReadPolicy:
BucketName: !Ref MyBucket
- Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref MySecret
AWS::Serverless::Api
Copy
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
# Enable tracing
TracingEnabled: true
# CORS configuration
Cors:
AllowMethods: "'GET,POST,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: "'https://myapp.com'"
# Custom domain
Domain:
DomainName: api.myapp.com
CertificateArn: !Ref Certificate
Route53:
HostedZoneId: Z1234567890
# Authentication
Auth:
DefaultAuthorizer: MyCognitoAuthorizer
Authorizers:
MyCognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
# Models and validation
Models:
OrderModel:
type: object
required:
- product_id
- quantity
properties:
product_id:
type: string
quantity:
type: integer
minimum: 1
AWS::Serverless::HttpApi
Copy
# HTTP API (API Gateway v2) - simpler, cheaper
MyHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: prod
CorsConfiguration:
AllowOrigins:
- "https://myapp.com"
AllowMethods:
- GET
- POST
AllowHeaders:
- Content-Type
MyFunction:
Type: AWS::Serverless::Function
Properties:
Events:
HttpApiEvent:
Type: HttpApi
Properties:
ApiId: !Ref MyHttpApi
Path: /items
Method: GET
Other SAM Resources
Copy
# Step Functions
MyStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/definition.asl.json
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref ProcessFunction
# Application (nested stack)
MyNestedApp:
Type: AWS::Serverless::Application
Properties:
Location:
ApplicationId: arn:aws:serverlessrepo:us-east-1:123456789012:applications/my-app
SemanticVersion: 1.0.0
# Layer
MyLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: my-dependencies
ContentUri: layers/
CompatibleRuntimes:
- python3.11
RetentionPolicy: Retain
SAM CLI Commands
Copy
┌────────────────────────────────────────────────────────────────────────┐
│ SAM CLI Workflow │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Development Workflow: │
│ ───────────────────── │
│ │
│ sam init # Create new project from template │
│ │ │
│ ▼ │
│ sam build # Build and package application │
│ │ │
│ ▼ │
│ sam local invoke # Test single function locally │
│ sam local start-api # Start local API Gateway │
│ │ │
│ ▼ │
│ sam validate # Validate template │
│ │ │
│ ▼ │
│ sam deploy --guided # Deploy to AWS (first time) │
│ sam deploy # Deploy using saved config │
│ │ │
│ ▼ │
│ sam logs -n MyFunction # View CloudWatch logs │
│ sam traces # View X-Ray traces │
│ │
└────────────────────────────────────────────────────────────────────────┘
Local Development
Copy
# Initialize new project
sam init --runtime python3.11 --name my-app
# Build the application
sam build
# Start local API Gateway
sam local start-api --port 3000
# Invoke function with event file
sam local invoke MyFunction --event events/order.json
# Generate sample event
sam local generate-event apigateway aws-proxy > events/api.json
sam local generate-event s3 put > events/s3.json
# Start Lambda with hot reload
sam local start-lambda --host 0.0.0.0
# Debug with VS Code (attach debugger on port 5858)
sam local invoke -d 5858 MyFunction --event event.json
Deployment
Copy
# First deployment (interactive)
sam deploy --guided
# Prompts for:
# - Stack name
# - Region
# - Confirm IAM role creation
# - Allow SAM to create resources
# Subsequent deployments
sam deploy
# Deploy to specific environment
sam deploy --config-env prod
# Deploy with parameter overrides
sam deploy --parameter-overrides Environment=prod LogLevel=WARN
# View deployed stack
sam list stacks
# Delete stack
sam delete --stack-name my-stack
Debugging and Monitoring
Copy
# Tail logs in real-time
sam logs -n MyFunction --tail
# Get logs for specific time range
sam logs -n MyFunction --start-time "5 minutes ago"
# View X-Ray traces
sam traces
# Sync changes in real-time (development)
sam sync --watch --stack-name my-stack
Project Structure
Copy
my-serverless-app/
├── template.yaml # SAM template
├── samconfig.toml # Deployment configuration
├── src/
│ ├── orders/
│ │ ├── __init__.py
│ │ ├── app.py # Lambda handler
│ │ └── requirements.txt
│ ├── payments/
│ │ ├── app.py
│ │ └── requirements.txt
│ └── shared/
│ └── utils.py
├── layers/
│ └── python/
│ └── requirements.txt
├── events/
│ ├── order_created.json
│ └── api_request.json
├── tests/
│ ├── unit/
│ │ └── test_handler.py
│ └── integration/
│ └── test_api.py
└── statemachines/
└── order_workflow.asl.json
samconfig.toml
Copy
version = 0.1
[default.deploy.parameters]
stack_name = "my-app-dev"
region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=dev"
[prod.deploy.parameters]
stack_name = "my-app-prod"
region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=prod"
Policy Templates
SAM provides policy templates for common use cases:Copy
Policies:
# DynamoDB
- DynamoDBCrudPolicy:
TableName: !Ref MyTable
- DynamoDBReadPolicy:
TableName: !Ref MyTable
- DynamoDBStreamReadPolicy:
TableName: !Ref MyTable
StreamName: !GetAtt MyTable.StreamArn
# S3
- S3ReadPolicy:
BucketName: !Ref MyBucket
- S3WritePolicy:
BucketName: !Ref MyBucket
- S3CrudPolicy:
BucketName: !Ref MyBucket
# SQS
- SQSPollerPolicy:
QueueName: !GetAtt MyQueue.QueueName
- SQSSendMessagePolicy:
QueueName: !GetAtt MyQueue.QueueName
# SNS
- SNSPublishMessagePolicy:
TopicName: !GetAtt MyTopic.TopicName
# Lambda
- LambdaInvokePolicy:
FunctionName: !Ref OtherFunction
# Secrets Manager
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn: !Ref MySecret
# Step Functions
- StepFunctionsExecutionPolicy:
StateMachineName: !GetAtt MyStateMachine.Name
# EventBridge
- EventBridgePutEventsPolicy:
EventBusName: default
Testing
Unit Tests
Copy
# tests/unit/test_handler.py
import pytest
from unittest.mock import patch, MagicMock
from src.orders.app import lambda_handler
@pytest.fixture
def api_gateway_event():
return {
"httpMethod": "POST",
"path": "/orders",
"body": '{"product_id": "P001", "quantity": 2}',
"requestContext": {
"requestId": "test-request-id"
}
}
@pytest.fixture
def lambda_context():
context = MagicMock()
context.function_name = "test-function"
context.memory_limit_in_mb = 256
context.invoked_function_arn = "arn:aws:lambda:us-east-1:123456789012:function:test"
context.aws_request_id = "test-request-id"
return context
@patch('src.orders.app.dynamodb_table')
def test_create_order_success(mock_table, api_gateway_event, lambda_context):
mock_table.put_item.return_value = {}
response = lambda_handler(api_gateway_event, lambda_context)
assert response['statusCode'] == 201
mock_table.put_item.assert_called_once()
@patch('src.orders.app.dynamodb_table')
def test_create_order_validation_error(mock_table, api_gateway_event, lambda_context):
api_gateway_event['body'] = '{"product_id": ""}' # Invalid
response = lambda_handler(api_gateway_event, lambda_context)
assert response['statusCode'] == 400
Integration Tests
Copy
# tests/integration/test_api.py
import requests
import pytest
import os
API_URL = os.environ.get('API_URL', 'https://xxx.execute-api.us-east-1.amazonaws.com/Prod')
@pytest.mark.integration
def test_create_order_integration():
response = requests.post(
f"{API_URL}/orders",
json={"product_id": "P001", "quantity": 2},
headers={"Content-Type": "application/json"}
)
assert response.status_code == 201
data = response.json()
assert "order_id" in data
@pytest.mark.integration
def test_get_order_integration():
# First create an order
create_response = requests.post(
f"{API_URL}/orders",
json={"product_id": "P001", "quantity": 1}
)
order_id = create_response.json()["order_id"]
# Then retrieve it
get_response = requests.get(f"{API_URL}/orders/{order_id}")
assert get_response.status_code == 200
assert get_response.json()["order_id"] == order_id
CI/CD Integration
GitHub Actions
Copy
# .github/workflows/deploy.yml
name: Deploy SAM Application
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- uses: aws-actions/setup-sam@v2
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Build
run: sam build
- name: Run Tests
run: |
pip install pytest
pytest tests/unit/
- name: Deploy
run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset
Best Practices
Use Globals
Define common settings in Globals section to reduce repetition
Layer Dependencies
Put shared dependencies in Lambda Layers for faster deployments
Use Policy Templates
SAM policy templates follow least privilege by default
Local Testing
Always test locally before deploying—it’s faster and free
🎯 Interview Questions
Q1: SAM vs CDK vs Terraform?
Q1: SAM vs CDK vs Terraform?
SAM:
- Best for pure serverless applications
- Simplified syntax, built-in best practices
- Excellent local testing
- Programmatic (TypeScript, Python)
- Better for complex infrastructure
- More flexibility
- Multi-cloud support
- Established ecosystem
- State management
Q2: How does SAM local invoke work?
Q2: How does SAM local invoke work?
SAM CLI:
- Builds Docker container matching Lambda runtime
- Mounts your code into container
- Invokes handler with event
- Returns response
Next Module
AWS GuardDuty
Intelligent threat detection for your AWS environment