Introduction to the Serverless Framework
The Serverless Framework is an open-source tool that simplifies developing, deploying, and operating serverless applications across multiple cloud providers. While it supports various cloud providers, we'll focus on its integration with AWS in this lecture.
Analogy: Serverless Framework as a Universal Remote
Think of the Serverless Framework as a universal remote control for your cloud infrastructure:
- A universal remote replaces multiple device-specific remotes with a single interface
- The Serverless Framework replaces provider-specific tools with a unified approach
- Just as the universal remote has preset configurations for common devices, Serverless Framework offers templates for common architectures
- Both simplify complex operations into simple commands (e.g., "turn on TV" becomes "deploy")
- Both can be customized for specific needs while maintaining a consistent user experience
Setting Up the Serverless Framework
Before we start building, let's set up the Serverless Framework and configure it for AWS:
Installation
# Install the Serverless Framework globally
npm install -g serverless
# Verify installation
serverless --version
# Output should show something like:
# Framework Core: X.Y.Z
# Plugin: X.Y.Z
# SDK: X.Y.Z
AWS Credentials Setup
The Serverless Framework requires AWS credentials to deploy resources:
# Configure AWS credentials using the serverless config command
serverless config credentials --provider aws --key YOUR_ACCESS_KEY --secret YOUR_SECRET_KEY
# Alternatively, use AWS CLI to configure credentials
aws configure
Best Practices for AWS Credentials
- Use IAM roles: For CI/CD pipelines and production environments
- Create dedicated IAM users: Don't use your root AWS account
- Principle of least privilege: Only grant necessary permissions
- Use different profiles: Separate credentials for different environments
- Regularly rotate credentials: Change access keys periodically
# Using a specific AWS profile
serverless deploy --aws-profile development
# Or set in serverless.yml
provider:
name: aws
profile: development
Creating a New Project
The Serverless Framework provides templates to help get started quickly:
# Create a new service with a specific template
serverless create --template aws-nodejs --path my-serverless-api
# Change to the new directory
cd my-serverless-api
# Check the template files
ls -la
The template creates a basic structure with a serverless.yml configuration file and a handler.js file containing a sample Lambda function.
Understanding serverless.yml
The serverless.yml file is the core of your Serverless Framework project, defining all the resources and configurations:
# serverless.yml - Basic structure
service: my-serverless-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs16.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
profile: ${opt:profile, 'default'}
environment:
STAGE: ${self:provider.stage}
SERVICE_NAME: ${self:service}
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
plugins:
- serverless-offline
custom:
myVariable: myValue
Key Sections of serverless.yml
- service: Name of your service
- provider: Cloud provider configuration
- functions: Lambda functions and their triggers
- resources: Additional infrastructure resources
- plugins: Serverless Framework plugins
- custom: Custom variables and configurations
- package: Packaging configurations
Variables and References
The Serverless Framework provides a powerful variable system for dynamic configurations:
# serverless.yml - Variables example
service: ${env:SERVICE_NAME, 'default-service-name'}
provider:
name: aws
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
environment:
TABLE_NAME: ${self:service}-${self:provider.stage}-table
S3_BUCKET: ${cf:another-stack-${self:provider.stage}.BucketName}
SECRET_KEY: ${ssm:/my-app/${self:provider.stage}/secret-key}
custom:
domains:
dev: dev.api.example.com
prod: api.example.com
domain: ${self:custom.domains.${self:provider.stage}}
Variable Types
- ${self:xxx} - Reference a property of the serverless.yml
- ${opt:xxx, 'default'} - Command line options with default
- ${env:xxx} - Environment variables
- ${file(./path/to/file.json):propertyName} - External file
- ${cf:stackName.outputName} - CloudFormation stack outputs
- ${ssm:/path/to/param} - AWS Systems Manager Parameters
Building a RESTful API
Let's build a complete RESTful API for a todo application using the Serverless Framework:
Project Structure
# Create a folder structure
mkdir -p todo-api/src/{functions,models,libs}
cd todo-api
# Initialize npm
npm init -y
# Install dependencies
npm install --save aws-sdk uuid jsonschema
npm install --save-dev serverless-offline
Recommended Folder Structure
todo-api/
├── serverless.yml # Service configuration
├── package.json # Dependencies
├── .gitignore # Git ignore file
├── README.md # Documentation
├── src/
│ ├── functions/ # Lambda function handlers
│ │ ├── create.js
│ │ ├── list.js
│ │ ├── get.js
│ │ ├── update.js
│ │ └── delete.js
│ ├── models/ # Data models
│ │ └── todo.js
│ └── libs/ # Shared code
│ ├── dynamodb.js
│ ├── response.js
│ └── validator.js
└── tests/ # Test files
└── ...
Configuring the API in serverless.yml
# serverless.yml for the Todo API
service: todo-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs16.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
environment:
TODOS_TABLE: ${self:service}-${self:provider.stage}-todos
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: !GetAtt TodosTable.Arn
functions:
create:
handler: src/functions/create.handler
events:
- http:
path: todos
method: post
cors: true
list:
handler: src/functions/list.handler
events:
- http:
path: todos
method: get
cors: true
get:
handler: src/functions/get.handler
events:
- http:
path: todos/{id}
method: get
cors: true
update:
handler: src/functions/update.handler
events:
- http:
path: todos/{id}
method: put
cors: true
delete:
handler: src/functions/delete.handler
events:
- http:
path: todos/{id}
method: delete
cors: true
resources:
Resources:
TodosTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.TODOS_TABLE}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
plugins:
- serverless-offline
custom:
serverless-offline:
httpPort: 3000
Creating Shared Libraries
Let's create some shared libraries for our API:
// src/libs/dynamodb.js
const AWS = require('aws-sdk');
let options = {};
// If running offline, use local DynamoDB
if (process.env.IS_OFFLINE) {
options = {
region: 'localhost',
endpoint: 'http://localhost:8000'
};
}
const client = new AWS.DynamoDB.DocumentClient(options);
module.exports = client;
// src/libs/response.js
/**
* Create a standardized API response
*/
function response(statusCode, body) {
return {
statusCode,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
};
}
/**
* Success response helper
*/
function success(body) {
return response(200, body);
}
/**
* Error response helper
*/
function error(statusCode, message, details = null) {
const body = {
error: message
};
if (details) {
body.details = details;
}
return response(statusCode, body);
}
module.exports = {
response,
success,
error
};
// src/libs/validator.js
const { Validator } = require('jsonschema');
const validator = new Validator();
// Todo schema for validation
const todoSchema = {
type: 'object',
properties: {
title: { type: 'string', minLength: 1, maxLength: 100 },
description: { type: 'string', maxLength: 500 },
completed: { type: 'boolean' },
dueDate: { type: 'string', format: 'date-time' }
},
required: ['title'],
additionalProperties: false
};
/**
* Validate a todo item against the schema
*/
function validateTodo(todo) {
return validator.validate(todo, todoSchema);
}
module.exports = {
validateTodo
};
Implementing CRUD Functions
Let's implement the CRUD (Create, Read, Update, Delete) functions for our Todo API:
Create Function
// src/functions/create.js
const { v4: uuidv4 } = require('uuid');
const dynamoDB = require('../libs/dynamodb');
const { success, error } = require('../libs/response');
const { validateTodo } = require('../libs/validator');
module.exports.handler = async (event) => {
try {
// Parse request body
const body = JSON.parse(event.body);
// Validate todo
const validation = validateTodo(body);
if (!validation.valid) {
return error(400, 'Invalid todo data', validation.errors);
}
// Create todo item
const now = new Date().toISOString();
const todo = {
id: uuidv4(),
title: body.title,
description: body.description || '',
completed: body.completed || false,
dueDate: body.dueDate || null,
createdAt: now,
updatedAt: now
};
// Save to DynamoDB
await dynamoDB.put({
TableName: process.env.TODOS_TABLE,
Item: todo
}).promise();
return success(todo);
} catch (err) {
console.error('Error creating todo:', err);
return error(500, 'Could not create todo');
}
};
List Function
// src/functions/list.js
const dynamoDB = require('../libs/dynamodb');
const { success, error } = require('../libs/response');
module.exports.handler = async (event) => {
try {
// Extract query string parameters
const params = event.queryStringParameters || {};
// Basic scan operation
const result = await dynamoDB.scan({
TableName: process.env.TODOS_TABLE
}).promise();
// Apply filtering if needed
let todos = result.Items;
// Filter by completion status if specified
if (params.completed !== undefined) {
const isCompleted = params.completed === 'true';
todos = todos.filter(todo => todo.completed === isCompleted);
}
// Sort by due date or creation date
if (params.sort === 'dueDate') {
todos.sort((a, b) => {
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return new Date(a.dueDate) - new Date(b.dueDate);
});
} else {
todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
return success({
count: todos.length,
todos: todos
});
} catch (err) {
console.error('Error listing todos:', err);
return error(500, 'Could not list todos');
}
};
Get Function
// src/functions/get.js
const dynamoDB = require('../libs/dynamodb');
const { success, error } = require('../libs/response');
module.exports.handler = async (event) => {
try {
// Get todo ID from path parameters
const id = event.pathParameters.id;
// Fetch from DynamoDB
const result = await dynamoDB.get({
TableName: process.env.TODOS_TABLE,
Key: { id }
}).promise();
// Check if todo exists
if (!result.Item) {
return error(404, 'Todo not found');
}
return success(result.Item);
} catch (err) {
console.error('Error getting todo:', err);
return error(500, 'Could not get todo');
}
};
Update Function
// src/functions/update.js
const dynamoDB = require('../libs/dynamodb');
const { success, error } = require('../libs/response');
const { validateTodo } = require('../libs/validator');
module.exports.handler = async (event) => {
try {
// Get todo ID from path parameters
const id = event.pathParameters.id;
// Parse request body
const body = JSON.parse(event.body);
// Validate todo
const validation = validateTodo(body);
if (!validation.valid) {
return error(400, 'Invalid todo data', validation.errors);
}
// Check if todo exists
const existingTodo = await dynamoDB.get({
TableName: process.env.TODOS_TABLE,
Key: { id }
}).promise();
if (!existingTodo.Item) {
return error(404, 'Todo not found');
}
// Update todo
const updatedTodo = {
...existingTodo.Item,
title: body.title,
description: body.description || existingTodo.Item.description,
completed: body.completed !== undefined ? body.completed : existingTodo.Item.completed,
dueDate: body.dueDate !== undefined ? body.dueDate : existingTodo.Item.dueDate,
updatedAt: new Date().toISOString()
};
// Save to DynamoDB
await dynamoDB.put({
TableName: process.env.TODOS_TABLE,
Item: updatedTodo
}).promise();
return success(updatedTodo);
} catch (err) {
console.error('Error updating todo:', err);
return error(500, 'Could not update todo');
}
};
Delete Function
// src/functions/delete.js
const dynamoDB = require('../libs/dynamodb');
const { success, error } = require('../libs/response');
module.exports.handler = async (event) => {
try {
// Get todo ID from path parameters
const id = event.pathParameters.id;
// Check if todo exists
const existingTodo = await dynamoDB.get({
TableName: process.env.TODOS_TABLE,
Key: { id }
}).promise();
if (!existingTodo.Item) {
return error(404, 'Todo not found');
}
// Delete from DynamoDB
await dynamoDB.delete({
TableName: process.env.TODOS_TABLE,
Key: { id }
}).promise();
return success({
message: 'Todo deleted successfully',
id
});
} catch (err) {
console.error('Error deleting todo:', err);
return error(500, 'Could not delete todo');
}
};
Local Development and Testing
The Serverless Framework provides tools for local development and testing:
serverless-offline Plugin
This plugin emulates AWS Lambda and API Gateway locally:
# Start the local development server
serverless offline start
# You should see output like:
# Starting Offline: dev/us-east-1.
# Server ready: http://localhost:3000
Testing with curl
# Create a todo
curl -X POST \
http://localhost:3000/todos \
-H 'Content-Type: application/json' \
-d '{
"title": "Learn Serverless Framework",
"description": "Complete the tutorial and build a Todo API",
"dueDate": "2025-06-01T00:00:00Z"
}'
# List todos
curl http://localhost:3000/todos
# Get a specific todo
curl http://localhost:3000/todos/{id}
# Update a todo
curl -X PUT \
http://localhost:3000/todos/{id} \
-H 'Content-Type: application/json' \
-d '{
"title": "Learn Serverless Framework",
"description": "Tutorial completed!",
"completed": true
}'
# Delete a todo
curl -X DELETE http://localhost:3000/todos/{id}
Unit Testing
We can use Jest for unit testing our Lambda functions:
# Install Jest and testing libraries
npm install --save-dev jest aws-sdk-mock
# Create a jest.config.js file
echo '{
"testEnvironment": "node"
}' > jest.config.js
# Add test script to package.json
# "scripts": {
# "test": "jest"
# }
// tests/functions/create.test.js
const AWS = require('aws-sdk-mock');
const { v4: uuidv4 } = require('uuid');
const createHandler = require('../../src/functions/create').handler;
// Mock UUID generation to return a predictable value
jest.mock('uuid');
uuidv4.mockImplementation(() => 'test-uuid');
describe('Create Todo Function', () => {
beforeAll(() => {
// Mock process.env
process.env.TODOS_TABLE = 'test-todos-table';
});
beforeEach(() => {
// Reset all mocks before each test
AWS.restore();
});
test('should create a todo successfully', async () => {
// Mock DynamoDB put operation
AWS.mock('DynamoDB.DocumentClient', 'put', (params, callback) => {
callback(null, { });
});
// Mock event with valid todo data
const event = {
body: JSON.stringify({
title: 'Test Todo',
description: 'This is a test todo'
})
};
// Call the handler
const response = await createHandler(event);
// Verify response
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.id).toBe('test-uuid');
expect(body.title).toBe('Test Todo');
expect(body.description).toBe('This is a test todo');
expect(body.completed).toBe(false);
});
test('should return error for invalid todo data', async () => {
// Mock event with invalid todo data (missing title)
const event = {
body: JSON.stringify({
description: 'This is an invalid todo'
})
};
// Call the handler
const response = await createHandler(event);
// Verify response
expect(response.statusCode).toBe(400);
const body = JSON.parse(response.body);
expect(body.error).toBe('Invalid todo data');
});
test('should handle DynamoDB errors', async () => {
// Mock DynamoDB put operation to return an error
AWS.mock('DynamoDB.DocumentClient', 'put', (params, callback) => {
callback(new Error('DynamoDB error'));
});
// Mock event with valid todo data
const event = {
body: JSON.stringify({
title: 'Test Todo',
description: 'This is a test todo'
})
};
// Call the handler
const response = await createHandler(event);
// Verify response
expect(response.statusCode).toBe(500);
const body = JSON.parse(response.body);
expect(body.error).toBe('Could not create todo');
});
});
Deployment and CI/CD
The Serverless Framework makes deployment straightforward:
Deploying to AWS
# Deploy to the default stage (dev)
serverless deploy
# Deploy to a specific stage
serverless deploy --stage production
# Deploy a single function
serverless deploy function --function create
Deployment Process
CI/CD Integration
Integrating the Serverless Framework into CI/CD pipelines:
# GitHub Actions Workflow Example (.github/workflows/deploy.yml)
name: Deploy Serverless API
on:
push:
branches:
- main
- develop
jobs:
deploy:
name: deploy
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Set deployment stage
id: set-stage
run: |
if [[ ${{ github.ref }} == 'refs/heads/main' ]]; then
echo "STAGE=prod" >> $GITHUB_ENV
else
echo "STAGE=dev" >> $GITHUB_ENV
fi
- name: Serverless Deploy
uses: serverless/github-action@v3
with:
args: deploy --stage ${{ env.STAGE }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
CI/CD Best Practices
- Environment-specific deployments: Use different stages for dev, staging, production
- Automated testing: Run tests before deployment
- Infrastructure validation: Use the
serverless packagecommand to validate the generated CloudFormation template - Rollback strategy: Implement automatic rollback on failure
- Security scanning: Integrate security scanning tools
Advanced Serverless Features
Let's explore some advanced features of the Serverless Framework:
Custom Domains
Using the serverless-domain-manager plugin to configure custom domains:
# Install the plugin
npm install --save-dev serverless-domain-manager
# Update serverless.yml
plugins:
- serverless-domain-manager
custom:
customDomain:
domainName: api.example.com
basePath: ''
stage: ${self:provider.stage}
createRoute53Record: true
endpointType: 'regional'
securityPolicy: tls_1_2
apiType: rest
autoDomain: true
# Create the domain
serverless create_domain
# Deploy the service
serverless deploy
API Authorization
Adding authorization to your API endpoints:
# serverless.yml - API with Cognito authorizer
resources:
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ${self:service}-${self:provider.stage}-user-pool
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:service}-${self:provider.stage}-user-pool-client
UserPoolId:
Ref: CognitoUserPool
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
functions:
create:
handler: src/functions/create.handler
events:
- http:
path: todos
method: post
cors: true
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
Ref: ApiGatewayAuthorizer
# Other functions...
ApiGatewayAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: cognito-authorizer
IdentitySource: method.request.header.Authorization
RestApiId:
Ref: ApiGatewayRestApi
Type: COGNITO_USER_POOLS
ProviderARNs:
- !GetAtt CognitoUserPool.Arn
Environment Secrets Management
Using AWS Parameter Store for secure parameter management:
# serverless.yml - Using SSM Parameters
provider:
name: aws
environment:
# Reference a parameter directly
API_KEY: ${ssm:/my-app/${self:provider.stage}/api-key}
# Reference a secure string parameter (decrypted)
DATABASE_PASSWORD: ${ssm:/my-app/${self:provider.stage}/db-password~true}
# Reference with a default value
API_TIMEOUT: ${ssm:/my-app/${self:provider.stage}/api-timeout, '30'}
# Creating parameters with AWS CLI
aws ssm put-parameter \
--name "/my-app/dev/api-key" \
--value "my-api-key" \
--type String
aws ssm put-parameter \
--name "/my-app/dev/db-password" \
--value "my-secure-password" \
--type SecureString
Handling Scheduled Tasks
Creating Lambda functions triggered by scheduled events:
# serverless.yml - Scheduled events
functions:
dailyCleanup:
handler: src/functions/cleanup.handler
events:
- schedule:
rate: cron(0 1 * * ? *) # Run at 1 AM UTC every day
enabled: true
input:
maxAge: 30 # Delete todos older than 30 days
remindDueTodos:
handler: src/functions/remind.handler
events:
- schedule:
rate: rate(1 hour) # Run every hour
enabled: true
# Scheduled function implementation
// src/functions/cleanup.handler
module.exports.handler = async (event) => {
console.log('Cleanup event:', event);
// Get the max age from the event input or use default
const maxAge = event.maxAge || 30;
// Calculate the cutoff date
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - maxAge);
// Find and delete old todos
// Implementation...
};
Performance Optimization and Monitoring
Optimizing and monitoring your serverless applications is crucial for production deployments:
Cold Start Optimization
# serverless.yml - Lambda provisioned concurrency
functions:
create:
handler: src/functions/create.handler
events:
- http:
path: todos
method: post
cors: true
# Keep 5 instances warm for critical functions
provisionedConcurrency: 5
Cold Start Reduction Techniques
- Choose the right runtime: Node.js and Python have faster startup times
- Minimize deployment package size: Include only necessary dependencies
- Separate critical and non-critical functions: Apply provisioned concurrency only where needed
- Implement connection reuse: Initialize clients outside the handler
- Implement periodic warming: Use CloudWatch Events to ping critical functions
Monitoring and Debugging
Using the serverless-plugin-aws-alerts plugin for CloudWatch Alarms:
# Install the plugin
npm install --save-dev serverless-plugin-aws-alerts
# Update serverless.yml
plugins:
- serverless-plugin-aws-alerts
custom:
alerts:
stages:
- production
topics:
alarm:
topic: ${self:service}-${self:provider.stage}-alerts
notifications:
- protocol: email
endpoint: alerts@example.com
alarms:
- functionErrors
- functionThrottles
- functionInvocations
- functionDuration
functions:
create:
handler: src/functions/create.handler
events:
- http:
path: todos
method: post
alarms:
- name: functionErrors
threshold: 1
evaluationPeriods: 1
period: 60
- name: functionDuration
threshold: 1000
evaluationPeriods: 1
period: 60
X-Ray Integration
Enabling AWS X-Ray for distributed tracing:
# serverless.yml - X-Ray tracing
provider:
name: aws
tracing:
apiGateway: true
lambda: true
# Lambda function with X-Ray tracing
// src/functions/create.js with X-Ray
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
// The rest of your function code...
Multi-Region and Disaster Recovery
Building resilient serverless applications with multi-region deployment:
Multi-Region Strategy
# Deploy to multiple regions
serverless deploy --region us-east-1
serverless deploy --region eu-west-1
serverless deploy --region ap-southeast-1
# Create a package for multi-region deployment
serverless package --region us-east-1 --package us-east-1-package
serverless package --region eu-west-1 --package eu-west-1-package
serverless package --region ap-southeast-1 --package ap-southeast-1-package
# Deploy packages
serverless deploy --package us-east-1-package --region us-east-1
serverless deploy --package eu-west-1-package --region eu-west-1
serverless deploy --package ap-southeast-1-package --region ap-southeast-1
Global DynamoDB Replication
# serverless.yml - Global DynamoDB table
resources:
Resources:
TodosTable:
Type: AWS::DynamoDB::GlobalTable
Properties:
TableName: ${self:provider.environment.TODOS_TABLE}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Replicas:
- Region: us-east-1
- Region: eu-west-1
- Region: ap-southeast-1
Route 53 Failover Routing
Using Route 53 for failover between regions:
# Setup AWS CLI health checks and failover records
aws route53 create-health-check \
--caller-reference $(date +%s) \
--health-check-config '{
"FullyQualifiedDomainName": "api-us-east-1.example.com",
"Port": 443,
"Type": "HTTPS",
"ResourcePath": "/health",
"RequestInterval": 30,
"FailureThreshold": 3
}'
# Note the returned HealthCheckId
aws route53 change-resource-record-sets \
--hosted-zone-id YOUR_HOSTED_ZONE_ID \
--change-batch '{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "api.example.com",
"Type": "A",
"SetIdentifier": "Primary",
"Failover": "PRIMARY",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d-1234567890.execute-api.us-east-1.amazonaws.com",
"EvaluateTargetHealth": true
},
"HealthCheckId": "YOUR_HEALTH_CHECK_ID"
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "api.example.com",
"Type": "A",
"SetIdentifier": "Secondary",
"Failover": "SECONDARY",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d-0987654321.execute-api.eu-west-1.amazonaws.com",
"EvaluateTargetHealth": true
}
}
}
]
}'
Cost Optimization
Strategies to optimize costs for serverless applications:
Right-Sizing Lambda Functions
Memory vs. Performance Analysis
| Memory (MB) | Avg. Duration (ms) | Cost per 1M Invocations | Notes |
|---|---|---|---|
| 128 | 350 | $0.73 | Lowest memory, slowest performance |
| 256 | 180 | $0.75 | Better cost-performance ratio |
| 512 | 120 | $1.00 | Good balance for most functions |
| 1024 | 80 | $1.35 | Higher cost but better user experience |
| 2048 | 60 | $2.00 | Diminishing returns on performance |
API Gateway Caching
# serverless.yml - API Gateway caching
provider:
name: aws
apiGateway:
apiKeys:
- ${self:service}-${self:provider.stage}-key
usagePlan:
quota:
limit: 5000
period: DAY
throttle:
burstLimit: 200
rateLimit: 100
# Enable caching
cacheEnabled: true
cacheSize: '0.5' # Cache size in GB
Lambda Reservation for Predictable Workloads
# Using Lambda provisioned concurrency for consistent pricing
functions:
highTrafficFunction:
handler: src/functions/highTraffic.handler
provisionedConcurrency: 10
Cost Optimization Best Practices
- Set appropriate timeouts: Avoid unnecessary execution time
- Optimize third-party dependencies: Use lightweight alternatives or bundle only what you need
- Share resources across functions: Use singleton patterns for database connections
- Implement caching: Cache external API responses and database queries
- Set up cost alarms: Get notified when costs exceed thresholds
Practical Exercise
Enhancing the Todo API
In this exercise, you'll enhance the Todo API with additional features:
- Add user authentication using Amazon Cognito
- Implement due date notifications using SQS and SNS
- Add a search endpoint using DynamoDB query operations
- Implement basic analytics for todo completion rates
- Add an export function to generate a CSV of todos
Task 1: User Authentication
Update the serverless.yml to include Cognito:
resources:
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ${self:service}-${self:provider.stage}-user-pool
AutoVerifiedAttributes:
- email
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:service}-${self:provider.stage}-user-pool-client
UserPoolId:
Ref: CognitoUserPool
GenerateSecret: false
ApiGatewayAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: cognito-authorizer
RestApiId:
Ref: ApiGatewayRestApi
Type: COGNITO_USER_POOLS
IdentitySource: method.request.header.Authorization
ProviderARNs:
- !GetAtt CognitoUserPool.Arn
Update the functions to use the authorizer:
functions:
create:
handler: src/functions/create.handler
events:
- http:
path: todos
method: post
cors: true
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
Ref: ApiGatewayAuthorizer
Update the create function to store user ID:
// Extract user ID from the Cognito JWT
const userId = event.requestContext.authorizer.claims.sub;
// Create todo with user ownership
const todo = {
id: uuidv4(),
userId: userId, // Store the Cognito user ID
title: body.title,
// ... other fields
};
Conclusion and Key Takeaways
- The Serverless Framework provides a powerful, multi-provider solution for developing and deploying serverless applications
- With a simple YAML configuration file, you can define complex serverless architectures including functions, events, and resources
- The framework abstracts away much of the complexity of working with CloudFormation and AWS services
- Local development is facilitated through plugins like serverless-offline
- Deployment is straightforward with a single command and supports different stages
- Advanced features include custom domains, authentication, monitoring, and multi-region support
- Best practices include proper error handling, monitoring, testing, and cost optimization
By leveraging the Serverless Framework, you can focus on writing business logic while the framework handles the infrastructure, enabling rapid development and deployment of scalable, cost-effective serverless applications.