skills/aws-lambda-typescript-integration/references/serverless-deployment.md

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