46 KiB
46 KiB
Deployment Strategies for NestJS Lambda
Overview
This reference covers deployment strategies, CI/CD pipelines, and optimization techniques for NestJS Lambda applications using AWS SAM and Serverless Framework.
AWS SAM (Serverless Application Model)
Installation
# macOS
brew tap aws/tap
brew install aws-sam-cli
# Linux
wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install
# Verify installation
sam --version
Project Structure
my-nestjs-lambda/
├── src/ # NestJS source code
├── dist/ # Compiled output
├── template.yaml # SAM template
├── samconfig.toml # SAM configuration
├── lambda.ts # Lambda entry point
├── package.json
└── tsconfig.json
SAM Template Reference
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: NestJS Lambda Application
Globals:
Function:
Timeout: 29
MemorySize: 512
Runtime: nodejs20.x
Architectures:
- x86_64
Environment:
Variables:
NODE_ENV: production
LOG_LEVEL: info
Tags:
Application: NestJS API
Environment: !Ref Environment
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
Description: Deployment environment
DatabaseUrl:
Type: String
NoEcho: true
Description: Database connection string
AllowedOrigins:
Type: String
Default: ""
Description: Comma-separated list of allowed CORS origins (leave empty to disable CORS)
Conditions:
IsProduction: !Equals [!Ref Environment, prod]
Resources:
# API Gateway
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
AllowOrigin: !Sub "'${AllowedOrigins}'"
MaxAge: "'600'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
Identity:
Header: Authorization
ValidationExpression: ^Bearer [-0-9a-zA-Z\._]*$
# Lambda Function
NestJSApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-api'
Handler: dist/lambda.handler
CodeUri: ./
Description: NestJS API Lambda function
Role: !GetAtt LambdaExecutionRole.Arn
Environment:
Variables:
DATABASE_URL: !Ref DatabaseUrl
JWT_SECRET: !Sub '{{resolve:secretsmanager:${JWTSecret}:SecretString:jwt}}'
COGNITO_USER_POOL_ID: !Ref UserPool
COGNITO_CLIENT_ID: !Ref UserPoolClient
Events:
ApiRoot:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /
Method: ANY
ApiProxy:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /{proxy+}
Method: ANY
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: !If [IsProduction, 5, 0]
AutoPublishAlias: live
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ErrorAlarm
- !Ref LatencyAlarm
Hooks:
PreTraffic: !Ref PreTrafficHookFunction
PostTraffic: !Ref PostTrafficHookFunction
# Lambda Execution Role
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/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: CloudWatchLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:DescribeLogStreams
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
- PolicyName: SecretsManagerPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref JWTSecret
- PolicyName: DynamoDBPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource: !GetAtt DynamoDBTable.Arn
- PolicyName: S3Policy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: !Sub '${S3Bucket.Arn}/*'
# VPC Configuration for RDS access
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Lambda functions
VpcId: !Ref VPC
SecurityGroupIngress: []
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: !Ref VPCCidr
# DynamoDB Table
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub '${AWS::StackName}-data'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: GSI1SK
KeyType: RANGE
Projection:
ProjectionType: ALL
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: !If [IsProduction, true, false]
# S3 Bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${AWS::StackName}-assets-${AWS::AccountId}'
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: ExpireOldVersions
Status: Enabled
NoncurrentVersionExpirationInDays: 30
# Secrets Manager
JWTSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub '${AWS::StackName}/jwt-secret'
Description: JWT signing secret
GenerateSecretString:
SecretStringTemplate: '{"jwt":""}'
GenerateStringKey: jwt
PasswordLength: 32
ExcludeCharacters: '"@/\'
# CloudWatch Alarms
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-errors'
AlarmDescription: Lambda error rate alarm
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref NestJSApiFunction
LatencyAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-latency'
AlarmDescription: Lambda duration alarm
MetricName: Duration
Namespace: AWS/Lambda
Statistic: p99
Period: 60
EvaluationPeriods: 2
Threshold: 5000
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref NestJSApiFunction
# CloudWatch Log Group
LambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/lambda/${NestJSApiFunction}'
RetentionInDays: !If [IsProduction, 30, 7]
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub 'https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/'
Export:
Name: !Sub '${AWS::StackName}-api-url'
FunctionArn:
Description: Lambda function ARN
Value: !GetAtt NestJSApiFunction.Arn
Export:
Name: !Sub '${AWS::StackName}-function-arn'
SAM Configuration (samconfig.toml)
version = 0.1
[default]
[default.global.parameters]
stack_name = "nestjs-lambda-api"
[default.build.parameters]
cached = true
parallel = true
[default.validate.parameters]
lint = true
[default.deploy.parameters]
capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND"
confirm_changeset = true
resolve_s3 = true
s3_prefix = "nestjs-lambda-api"
region = "us-east-1"
image_repositories = []
parameter_overrides = [
"Environment=dev"
]
[default.deploy.parameters.globals]
[default.deploy.parameters.globals.parameters]
parallel = true
[dev.deploy.parameters]
stack_name = "nestjs-lambda-api-dev"
parameter_overrides = [
"Environment=dev",
"DatabaseUrl=postgres://..."
]
[staging.deploy.parameters]
stack_name = "nestjs-lambda-api-staging"
parameter_overrides = [
"Environment=staging",
"DatabaseUrl=postgres://..."
]
[prod.deploy.parameters]
stack_name = "nestjs-lambda-api-prod"
parameter_overrides = [
"Environment=prod",
"DatabaseUrl=postgres://..."
]
[default.sync.parameters]
watch = true
[default.local_start_api.parameters]
warm_containers = "EAGER"
[default.local_start_lambda.parameters]
warm_containers = "EAGER"
SAM Commands
# Initialize SAM project
sam init --runtime nodejs20.x --dependency-manager npm --app-template hello-world
# Validate template
sam validate --lint
# Build application
sam build
# Local development
sam local invoke NestJSApiFunction -e events/api-event.json
sam local start-api --warm-containers EAGER
# Deploy
sam deploy --guided
sam deploy --config-env prod
# Sync (for development)
sam sync --watch
# Logs
sam logs -n NestJSApiFunction --tail
# Delete stack
sam delete
SAM Policy Templates
# Inline policy for specific AWS services
Policies:
- S3ReadPolicy:
BucketName: !Ref S3Bucket
- S3WritePolicy:
BucketName: !Ref S3Bucket
- DynamoDBReadPolicy:
TableName: !Ref DynamoDBTable
- DynamoDBCrudPolicy:
TableName: !Ref DynamoDBTable
- SESBulkTemplatedCrudPolicy:
IdentityName: !Ref SESIdentity
- SQSSendMessagePolicy:
QueueName: !GetAtt SQSQueue.QueueName
- SNSPublishMessagePolicy:
TopicName: !GetAtt SNSTopic.TopicName
- CloudWatchPutMetricPolicy: {}
- VPCAccessPolicy: {}
Serverless Framework
Installation
# Install Serverless Framework
npm install -g serverless
# Or use npx
npx serverless
# Verify installation
serverless --version
Project Structure
my-nestjs-lambda/
├── src/ # NestJS source code
├── dist/ # Compiled output
├── serverless.yml # Serverless configuration
├── serverless.ts # TypeScript config (optional)
├── lambda.ts # Lambda entry point
├── package.json
└── webpack.config.js
Serverless Configuration Reference
service: nestjs-lambda-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs20.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
memorySize: 512
timeout: 29
logRetentionInDays: 14
versionFunctions: true
# Environment variables
environment:
NODE_ENV: production
STAGE: ${self:provider.stage}
SERVICE_NAME: ${self:service}
DATABASE_URL: ${ssm:/${self:service}/${self:provider.stage}/database-url}
JWT_SECRET: ${ssm:/${self:service}/${self:provider.stage}/jwt-secret~true}
SENTRY_DSN: ${ssm:/${self:service}/${self:provider.stage}/sentry-dsn~true}
# IAM role statements
iam:
role:
name: ${self:service}-${self:provider.stage}-lambda-role
statements:
# CloudWatch Logs
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- 'arn:aws:logs:${aws:region}:${aws:accountId}:log-group:/aws/lambda/*:*:*'
# X-Ray Tracing
- Effect: Allow
Action:
- xray:PutTraceSegments
- xray:PutTelemetryRecords
Resource:
- !Sub 'arn:aws:xray:${aws:region}:${aws:accountId}:*'
# SSM Parameter Store
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource:
- 'arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/${self:service}/*'
# DynamoDB
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
- dynamodb:BatchGetItem
- dynamodb:BatchWriteItem
Resource:
- !GetAtt DynamoDBTable.Arn
- !Sub '${DynamoDBTable.Arn}/index/*'
# S3
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- !GetAtt S3Bucket.Arn
- !Sub '${S3Bucket.Arn}/*'
# SQS
- Effect: Allow
Action:
- sqs:SendMessage
- sqs:SendMessageBatch
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
Resource:
- !GetAtt SQSQueue.Arn
# SNS
- Effect: Allow
Action:
- sns:Publish
Resource:
- !Ref SNSTopic
# SES
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
- ses:SendTemplatedEmail
Resource:
- !Sub 'arn:aws:ses:${aws:region}:${aws:accountId}:identity/${self:custom.email.from}'
Condition:
StringEquals:
ses:FromAddress: ${self:custom.email.from}
# Secrets Manager
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- 'arn:aws:secretsmanager:${aws:region}:${aws:accountId}:secret:${self:service}/*'
# VPC configuration
vpc:
securityGroupIds:
- !Ref LambdaSecurityGroup
subnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
# API Gateway
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
# Tracing
tracing:
lambda: true
apiGateway: true
# Tags
tags:
Service: ${self:service}
Stage: ${self:provider.stage}
ManagedBy: serverless
# Stack tags
stackTags:
Service: ${self:service}
Stage: ${self:provider.stage}
plugins:
- serverless-esbuild
- serverless-offline
- serverless-plugin-aws-alerts
- serverless-plugin-warmup
custom:
# esbuild configuration
esbuild:
bundle: true
minify: ${self:custom.isProduction}
sourcemap: true
target: node20
platform: node
format: cjs
mainFields:
- main
- module
external:
- '@nestjs/microservices'
- '@nestjs/websockets'
- 'class-transformer/storage'
- 'aws-sdk'
keepNames: true
splitting: false
concurrency: 10
packager: npm
installExtraArgs: ['--legacy-peer-deps']
# Offline configuration
serverless-offline:
httpPort: 3000
host: 0.0.0.0
lambdaPort: 3002
noPrependStageInUrl: true
# Warming configuration
warmup:
warmer:
enabled: ${self:custom.isProduction}
events:
- schedule: rate(5 minutes)
concurrency: 2
# Alerts configuration
alerts:
stages:
- prod
dashboards: true
topics:
alarm:
- !Ref AlarmTopic
alarms:
- functionErrors
- functionThrottles
- functionInvocations
- functionDuration
definitions:
functionErrors:
threshold: 1
statistic: Sum
period: 60
evaluationPeriods: 1
comparisonOperator: GreaterThanOrEqualToThreshold
functionDuration:
threshold: 5000
statistic: p99
period: 60
evaluationPeriods: 2
comparisonOperator: GreaterThanThreshold
# Custom variables
isProduction: !Equals ['${self:provider.stage}', 'prod']
email:
from: noreply@example.com
package:
individually: false
patterns:
- '!**/*'
- 'dist/**'
- '!dist/**/*.map'
- '!dist/**/*.spec.js'
- '!dist/**/*.test.js'
- '!dist/**/__tests__/**'
- 'package.json'
functions:
api:
name: ${self:service}-${self:provider.stage}-api
handler: dist/lambda.handler
description: NestJS API Lambda function
memorySize: 512
timeout: 29
reservedConcurrency: 100
provisionedConcurrency: ${self:custom.isProduction, 0}
environment:
FUNCTION_NAME: api
events:
- http:
path: /{proxy+}
method: ANY
cors:
origin: ${self:custom.cors.origins}
headers:
- Content-Type
- Authorization
- X-Amz-Date
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
allowCredentials: true
- http:
path: /
method: ANY
cors: true
iamRoleStatements:
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub 'arn:aws:execute-api:${aws:region}:${aws:accountId}:${ApiGatewayRestApiId}/*'
worker:
name: ${self:service}-${self:provider.stage}-worker
handler: dist/worker.handler
description: Background worker Lambda
memorySize: 256
timeout: 900
events:
- sqs:
arn:
Fn::GetAtt:
- SQSQueue
- Arn
batchSize: 10
maximumBatchingWindowInSeconds: 5
functionResponseType: ReportBatchItemFailures
destinationConfig:
onFailure:
destination:
Fn::GetAtt:
- DLQ
- Arn
scheduler:
name: ${self:service}-${self:provider.stage}-scheduler
handler: dist/scheduler.handler
description: Scheduled task Lambda
memorySize: 256
timeout: 300
events:
- schedule:
rate: rate(1 hour)
input:
task: cleanup
- schedule:
rate: cron(0 2 * * ? *)
input:
task: daily-report
resources:
Resources:
# DynamoDB Table
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: ${self:custom.isProduction}
SSESpecification:
SSEEnabled: true
Tags:
- Key: Service
Value: ${self:service}
- Key: Stage
Value: ${self:provider.stage}
# S3 Bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:service}-${self:provider.stage}-assets-${aws:accountId}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: TransitionToIA
Status: Enabled
TransitionInDays: 30
StorageClass: STANDARD_IA
- Id: DeleteOldVersions
Status: Enabled
NoncurrentVersionExpirationInDays: 30
# SQS Queue
SQSQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${self:provider.stage}-queue
VisibilityTimeout: 960
MessageRetentionPeriod: 1209600
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt:
- DLQ
- Arn
maxReceiveCount: 3
# Dead Letter Queue
DLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${self:provider.stage}-dlq
MessageRetentionPeriod: 1209600
# SNS Topic
SNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:service}-${self:provider.stage}-topic
# Alarm Topic
AlarmTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:service}-${self:provider.stage}-alarms
# Security Group
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: ${self:service}-${self:provider.stage}-lambda-sg
GroupDescription: Security group for Lambda functions
VpcId:
Ref: VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp:
Ref: VPCCidr
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:service}-${self:provider.stage}-restApiId
ApiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:service}-${self:provider.stage}-rootResourceId
Serverless TypeScript Configuration
// serverless.ts
import type { AWS } from '@serverless/typescript';
const serverlessConfiguration: AWS = {
service: 'nestjs-lambda-api',
frameworkVersion: '3',
plugins: [
'serverless-esbuild',
'serverless-offline',
'serverless-plugin-aws-alerts',
],
provider: {
name: 'aws',
runtime: 'nodejs20.x',
stage: '${opt:stage, "dev"}',
region: '${opt:region, "us-east-1"}',
memorySize: 512,
timeout: 29,
environment: {
NODE_ENV: 'production',
DATABASE_URL: '${ssm:/${self:service}/${self:provider.stage}/database-url}',
},
iam: {
role: {
statements: [
{
Effect: 'Allow',
Action: ['logs:*'],
Resource: {
'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${self:service}-*',
},
},
],
},
},
},
functions: {
api: {
handler: 'dist/lambda.handler',
events: [
{
http: {
path: '/{proxy+}',
method: 'ANY',
cors: true,
},
},
],
},
},
custom: {
esbuild: {
bundle: true,
minify: true,
target: 'node20',
},
},
};
module.exports = serverlessConfiguration;
Serverless Commands
# Deploy
serverless deploy
serverless deploy --stage prod --region eu-west-1
serverless deploy function -f api
# Offline development
serverless offline
serverless offline start --reloadHandler
# Logs
serverless logs -f api -t
serverless logs -f api --startTime 5m
# Info
serverless info
serverless info --stage prod
# Remove
serverless remove
serverless remove --stage dev
# Print compiled config
serverless print
# Invoke function
serverless invoke -f api -p event.json
serverless invoke local -f api -p event.json
# Metrics
serverless metrics
serverless metrics -f api --startTime 1h
# Plugins
serverless plugin install -n serverless-esbuild
serverless plugin uninstall -n serverless-esbuild
serverless plugin list
serverless plugin search esbuild
CI/CD Pipeline
GitHub Actions with SAM
# .github/workflows/sam-deploy.yml
name: SAM Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
NODE_VERSION: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run lint
run: npm run lint
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SAM
uses: aws-actions/setup-sam@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: SAM Build
run: sam build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: sam-build
path: |
.aws-sam/
dist/
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- name: Setup SAM
uses: aws-actions/setup-sam@v2
- name: Configure AWS credentials
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: ${{ env.AWS_REGION }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: sam-build
- name: SAM Deploy (Dev)
run: |
sam deploy --config-env dev \
--no-confirm-changeset \
--no-fail-on-empty-changeset
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup SAM
uses: aws-actions/setup-sam@v2
- name: Configure AWS credentials
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: ${{ env.AWS_REGION }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: sam-build
- name: SAM Deploy (Prod)
run: |
sam deploy --config-env prod \
--no-confirm-changeset \
--no-fail-on-empty-changeset
GitHub Actions with Serverless Framework
# .github/workflows/serverless-deploy.yml
name: Serverless Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
NODE_VERSION: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run lint
run: npm run lint
deploy-dev:
needs: test
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install Serverless
run: npm install -g serverless
- name: Configure AWS credentials
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: ${{ env.AWS_REGION }}
- name: Deploy to dev
run: serverless deploy --stage dev
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install Serverless
run: npm install -g serverless
- name: Configure AWS credentials
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: ${{ env.AWS_REGION }}
- name: Deploy to production
run: serverless deploy --stage prod
Build Optimization
Webpack Configuration
// webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const TerserPlugin = require('terser-webpack-plugin');
const { IgnorePlugin } = require('webpack');
module.exports = {
entry: './lambda.ts',
target: 'node',
mode: 'production',
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
},
},
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
output: {
filename: 'lambda.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2',
clean: true,
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
keep_classnames: true,
keep_fnames: true,
compress: {
drop_console: true,
drop_debugger: true,
},
},
}),
],
},
plugins: [
// Ignore optional dependencies that aren't needed in Lambda
new IgnorePlugin({
resourceRegExp: /^@nestjs\/microservices$/,
}),
new IgnorePlugin({
resourceRegExp: /^@nestjs\/websockets$/,
}),
],
stats: {
modules: true,
warnings: false,
},
};
esbuild Configuration
// esbuild.config.js
const esbuild = require('esbuild');
const { nodeExternalsPlugin } = require('esbuild-node-externals');
async function build() {
try {
await esbuild.build({
entryPoints: ['lambda.ts', 'worker.ts', 'scheduler.ts'],
bundle: true,
platform: 'node',
target: 'node20',
outdir: 'dist',
minify: process.env.NODE_ENV === 'production',
sourcemap: true,
splitting: false,
format: 'cjs',
metafile: true,
external: [
'@nestjs/microservices',
'@nestjs/websockets',
'class-transformer/storage',
'aws-sdk', // Provided by Lambda runtime
],
define: {
'process.env.NODE_ENV': '"production"',
},
plugins: [
nodeExternalsPlugin({
allowList: ['@nestjs/core', '@nestjs/common', '@nestjs/platform-express'],
}),
],
});
console.log('Build completed successfully');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
build();
esbuild with Serverless
# serverless.yml with esbuild
custom:
esbuild:
bundle: true
minify: ${self:custom.isProduction}
sourcemap: true
target: node20
platform: node
format: cjs
mainFields:
- main
- module
external:
- '@nestjs/microservices'
- '@nestjs/websockets'
- 'class-transformer/storage'
- 'aws-sdk'
keepNames: true
splitting: false
concurrency: 10
packager: npm
installExtraArgs:
- '--legacy-peer-deps'
# Exclude specific paths from bundling
exclude:
- './test/**'
- './**/*.spec.ts'
- './**/*.test.ts'
Package Optimization
Dependencies to Exclude
{
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@codegenie/serverless-express": "^4.0.0",
"aws-lambda": "^1.0.0",
"express": "^4.18.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/testing": "^10.0.0",
"@types/aws-lambda": "^8.10.0",
"@types/express": "^4.17.0",
"@types/node": "^20.0.0",
"esbuild": "^0.20.0",
"serverless": "^3.0.0",
"serverless-esbuild": "^1.0.0",
"serverless-offline": "^13.0.0",
"ts-loader": "^9.0.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-node-externals": "^3.0.0"
}
}
SAM Package Configuration
# template.yaml - package optimization
Globals:
Function:
CodeUri: ./
Runtime: nodejs20.x
Architectures:
- x86_64
# Exclude dev dependencies
Environment:
Variables:
NODE_ENV: production
Resources:
NestJSApiFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2020
Sourcemap: true
EntryPoints:
- lambda.ts
External:
- '@nestjs/microservices'
- '@nestjs/websockets'
Properties:
Handler: dist/lambda.handler
Environment Management
SAM Environment Variables
# Using parameter overrides
Parameters:
Environment:
Type: String
Default: dev
# Secure parameters
DatabaseUrl:
Type: String
NoEcho: true
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
STAGE: !Ref Environment
DATABASE_URL: !Ref DatabaseUrl
SECRET_VALUE: !Sub '{{resolve:secretsmanager:${MySecret}:SecretString:value}}'
SSM_VALUE: !Sub '{{resolve:ssm-secure:/${Environment}/my-param}}'
SSM Parameter Store
# serverless.yml - SSM integration
provider:
environment:
# Standard parameters
API_URL: ${ssm:/${self:service}/${self:provider.stage}/api-url}
# Secure parameters (encrypted)
DATABASE_URL: ${ssm:/${self:service}/${self:provider.stage}/database-url~true}
# Reference by ARN
EXTERNAL_API_KEY: ${ssm:arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/external/api-key}
iam:
role:
statements:
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource:
- arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/${self:service}/*
Secrets Manager
# SAM with Secrets Manager
Resources:
MySecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub '${AWS::StackName}/my-secret'
GenerateSecretString:
SecretStringTemplate: '{"username":"admin"}'
GenerateStringKey: password
PasswordLength: 32
ExcludeCharacters: '"@/\'
MyFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
DB_SECRET: !Sub '{{resolve:secretsmanager:${MySecret}:SecretString}}'
# Serverless with Secrets Manager
provider:
environment:
# Full secret JSON
DB_SECRET: ${secrets:${self:service}/${self:provider.stage}/database}
# Specific key from secret
DB_PASSWORD: ${secrets:${self:service}/${self:provider.stage}/database:password}
iam:
role:
statements:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- arn:aws:secretsmanager:${aws:region}:${aws:accountId}:secret:${self:service}/*
Monitoring
SAM CloudWatch Dashboard
Resources:
MonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: !Sub '${AWS::StackName}-dashboard'
DashboardBody: !Sub |
{
"widgets": [
{
"type": "metric",
"properties": {
"title": "Invocations",
"metrics": [
["AWS/Lambda", "Invocations", "FunctionName", "${NestJSApiFunction}", { "stat": "Sum" }]
],
"period": 300,
"region": "${AWS::Region}",
"yAxis": { "left": { "min": 0 } }
}
},
{
"type": "metric",
"properties": {
"title": "Errors",
"metrics": [
["AWS/Lambda", "Errors", "FunctionName", "${NestJSApiFunction}", { "stat": "Sum" }],
[".", "Throttles", ".", ".", { "stat": "Sum" }]
],
"period": 300,
"region": "${AWS::Region}"
}
},
{
"type": "metric",
"properties": {
"title": "Duration",
"metrics": [
["AWS/Lambda", "Duration", "FunctionName", "${NestJSApiFunction}", { "stat": "p99" }],
["...", { "stat": "Average" }]
],
"period": 300,
"region": "${AWS::Region}"
}
}
]
}
Serverless CloudWatch Alarms
custom:
alerts:
stages:
- prod
- staging
dashboards: true
topics:
alarm:
- !Ref AlarmTopic
ok:
- !Ref OkTopic
alarms:
- functionErrors
- functionThrottles
- functionInvocations
- functionDuration
definitions:
functionErrors:
description: 'Function errors exceeded threshold'
threshold: 1
statistic: Sum
period: 60
evaluationPeriods: 1
comparisonOperator: GreaterThanOrEqualToThreshold
treatMissingData: notBreaching
functionThrottles:
description: 'Function throttled'
threshold: 1
statistic: Sum
period: 60
evaluationPeriods: 1
comparisonOperator: GreaterThanOrEqualToThreshold
functionDuration:
description: 'Function duration exceeded threshold'
threshold: 5000
statistic: p99
period: 60
evaluationPeriods: 2
comparisonOperator: GreaterThanThreshold
customAlarm:
description: 'Custom business metric'
metric: CustomMetric
namespace: MyNamespace
threshold: 100
statistic: Average
period: 300
evaluationPeriods: 1
X-Ray Tracing
# SAM - Enable tracing
Globals:
Function:
Tracing: Active
Resources:
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
TracingEnabled: true
# Serverless - Enable tracing
provider:
tracing:
lambda: true
apiGateway: true
// X-Ray instrumentation
import * as AWSXRay from 'aws-xray-sdk-core';
import * as AWS from 'aws-sdk';
// Capture AWS SDK calls
const tracedAWS = AWSXRay.captureAWS(AWS);
// Capture HTTP requests
import http from 'http';
import https from 'https';
AWSXRay.captureHTTPsGlobal(http);
AWSXRay.captureHTTPsGlobal(https);
// Custom subsegment
const segment = AWSXRay.getSegment();
const subsegment = segment?.addNewSubsegment('custom-operation');
try {
// Your code
subsegment?.close();
} catch (error) {
subsegment?.close(error);
}
Performance Tuning
Memory Configuration
# SAM - Memory and CPU tuning
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 512
# Memory impacts CPU proportionally
# 1769MB = 1 vCPU
# 3008MB = ~1.7 vCPU
ComputeIntensiveFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 3008
# Serverless - Memory per function
functions:
api:
memorySize: 512
compute:
memorySize: 1024
health:
memorySize: 256
Provisioned Concurrency
# SAM - Provisioned Concurrency
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 10
AutoPublishAlias: live
# Serverless - Provisioned Concurrency
functions:
api:
provisionedConcurrency: 10
Reserved Concurrency
# SAM - Reserved Concurrency
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
ReservedConcurrentExecutions: 100
# Serverless - Reserved Concurrency
functions:
api:
reservedConcurrency: 100
Rollback Strategy
SAM Auto-Rollback
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
AutoPublishAlias: live
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ErrorAlarm
- !Ref LatencyAlarm
Hooks:
PreTraffic: !Ref PreTrafficHook
PostTraffic: !Ref PostTrafficHook
PreTrafficHook:
Type: AWS::Serverless::Function
Properties:
Handler: hooks.preTraffic
Runtime: nodejs20.x
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codedeploy:PutLifecycleEventHookExecutionStatus
Resource:
- !Sub 'arn:aws:codedeploy:${aws:region}:${aws:accountId}:deploymentgroup:${AWS::StackName}/*'
PostTrafficHook:
Type: AWS::Serverless::Function
Properties:
Handler: hooks.postTraffic
Runtime: nodejs20.x
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codedeploy:PutLifecycleEventHookExecutionStatus
Resource:
- !Sub 'arn:aws:codedeploy:${aws:region}:${aws:accountId}:deploymentgroup:${AWS::StackName}/*'
Serverless Canary
custom:
canary:
type: LinearCanary
alias: Live
linearTrafficShifting:
- intervalMinutes: 10
percentage: 10
- intervalMinutes: 10
percentage: 50
- intervalMinutes: 10
percentage: 100
alarms:
- functionErrors
- functionDuration
Security Best Practices
SAM VPC Configuration
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Policies:
- VPCAccessPolicy: {}
Serverless VPC
provider:
vpc:
securityGroupIds:
- !Ref LambdaSecurityGroup
subnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
IAM Least Privilege
# SAM - Granular IAM
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource:
- !GetAtt MyTable.Arn
Condition:
ForAllValues:StringEquals:
dynamodb:LeadingKeys:
- 'USER#${cognito-identity.amazonaws.com:sub}'
# Serverless - Granular IAM
provider:
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource:
- !GetAtt MyTable.Arn
Condition:
ForAllValues:StringEquals:
dynamodb:LeadingKeys:
- 'USER#${cognito-identity.amazonaws.com:sub}'
Cost Optimization
Graviton2 (ARM64)
# SAM - ARM64
Globals:
Function:
Architectures:
- arm64
# Serverless - ARM64
provider:
architecture: arm64
SAM Lambda Layers
Resources:
CommonLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: common-dependencies
ContentUri: layers/common/
CompatibleRuntimes:
- nodejs20.x
RetentionPolicy: Retain
MyFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
- !Ref CommonLayer
Serverless Lambda Layers
layers:
common:
path: layers/common
compatibleRuntimes:
- nodejs20.x
package:
patterns:
- 'node_modules/**'
functions:
api:
layers:
- { Ref: CommonLambdaLayer }
SAM vs Serverless Framework
| Feature | SAM | Serverless Framework |
|---|---|---|
| Native AWS | Yes | No (uses CloudFormation) |
| Local testing | sam local |
serverless-offline |
| CI/CD integration | AWS-native | Multi-cloud support |
| Syntax | YAML/JSON | YAML/TypeScript/JavaScript |
| Extensions | SAR, nested stacks | Plugins ecosystem |
| Debugging | SAM CLI | Serverless console |
When to Choose SAM
- Native AWS environment
- Team familiar with CloudFormation
- Need deep AWS service integration
- Want to use AWS CodePipeline/CodeBuild
When to Choose Serverless Framework
- Multi-cloud requirements
- Large plugin ecosystem needed
- Prefer TypeScript/JavaScript config
- Want easier local development
Complete Example Project
nestjs-lambda-sam/
├── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── lambda.ts
│ └── modules/
│ └── users/
├── dist/
├── template.yaml
├── samconfig.toml
├── package.json
└── tsconfig.json
nestjs-lambda-serverless/
├── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── lambda.ts
│ └── modules/
│ └── users/
├── dist/
├── serverless.yml
├── package.json
└── tsconfig.json