Node.js API Deployment on AWS: EC2, ECS, and Lambda Compared

Node.js, AWS, DevOps|SEPTEMBER 18, 2025|0 VIEWS
Complete comparison of AWS deployment options for Node.js APIs: EC2, ECS, and Lambda - pros, cons, costs, and implementation examples

Introduction

When deploying Node.js APIs on AWS, you have several powerful options to choose from: EC2 instances, Elastic Container Service (ECS), and Lambda functions. Each approach has distinct advantages, trade-offs, and optimal use cases that can significantly impact your application's performance, scalability, and cost.

This comprehensive guide compares these three deployment strategies in-depth, providing practical examples, cost analysis, and implementation details to help you make the best choice for your Node.js API project.

AWS Deployment Options Overview

The Three Pillars of AWS Compute

// Deployment comparison at a glance
const AwsDeploymentOptions = {
  ec2: {
    type: "Infrastructure as a Service (IaaS)",
    control: "Full server control",
    complexity: "High",
    scalability: "Manual/Auto Scaling Groups",
    costModel: "Pay for running instances",
  },
  ecs: {
    type: "Container as a Service (CaaS)",
    control: "Container orchestration",
    complexity: "Medium",
    scalability: "Auto scaling containers",
    costModel: "Pay for compute resources",
  },
  lambda: {
    type: "Function as a Service (FaaS)",
    control: "Event-driven execution",
    complexity: "Low",
    scalability: "Automatic",
    costModel: "Pay per invocation",
  },
};

Each option serves different needs:

  • EC2: Full control over virtual machines
  • ECS: Container orchestration without managing servers
  • Lambda: Serverless functions that scale automatically

AWS EC2: Virtual Machine Deployment

Overview

Amazon EC2 (Elastic Compute Cloud) provides virtual servers where you have complete control over the operating system, runtime environment, and application deployment. This traditional approach offers maximum flexibility but requires more management overhead.

Pros and Cons

Advantages

  • Complete Control: Full access to the operating system and server configuration
  • Performance Predictability: Dedicated resources ensure consistent performance
  • Custom Dependencies: Install any software or system-level dependencies
  • Long-Running Processes: Perfect for background tasks, websockets, or persistent connections
  • Cost Efficiency for Steady Traffic: Reserved instances offer significant savings for predictable workloads

Disadvantages

  • Management Overhead: Requires patching, monitoring, and server maintenance
  • Scaling Complexity: Manual setup of load balancers and auto-scaling groups
  • Higher Minimum Cost: Always paying for running instances, even during low traffic
  • Security Responsibility: Full responsibility for OS-level security

Implementation Example

Basic Express.js API

// server.js - Simple Node.js API for EC2
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());

// Health check endpoint
app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
  });
});

// API routes
app.get("/api/users", (req, res) => {
  // Your API logic here
  res.json({ users: [] });
});

app.post("/api/users", (req, res) => {
  // Create user logic
  res.status(201).json({ message: "User created" });
});

// Error handling middleware
app.use((error, req, res, next) => {
  console.error("Error:", error);
  res.status(500).json({ error: "Internal server error" });
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`Server running on port ${PORT}`);
});

PM2 Configuration for Production

// ecosystem.config.js - PM2 configuration for production
module.exports = {
  apps: [
    {
      name: "nodejs-api",
      script: "./server.js",
      instances: "max", // Use all available CPU cores
      exec_mode: "cluster",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
      error_file: "./logs/err.log",
      out_file: "./logs/out.log",
      log_file: "./logs/combined.log",
      time: true,
      max_memory_restart: "1G",
      node_args: "--max-old-space-size=1024",
    },
  ],
};

User Data Script for Auto Deployment

#!/bin/bash
# EC2 user data script for automatic deployment

# Update system
yum update -y

# Install Node.js
curl -fsSL https://rpm.nodesource.com/setup_18.x | bash -
yum install -y nodejs

# Install PM2 globally
npm install -g pm2

# Create application directory
mkdir -p /opt/nodejs-api
cd /opt/nodejs-api

# Clone your repository (replace with your repo)
yum install -y git
git clone https://github.com/yourusername/your-api.git .

# Install dependencies
npm ci --only=production

# Create logs directory
mkdir -p logs

# Set up PM2 to start on boot
pm2 startup
pm2 start ecosystem.config.js
pm2 save

# Install Nginx as reverse proxy
amazon-linux-extras install -y nginx1
systemctl start nginx
systemctl enable nginx

# Configure Nginx (basic configuration)
cat > /etc/nginx/conf.d/nodejs-api.conf << 'EOF'
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
EOF

systemctl restart nginx

Cost Analysis for EC2

// EC2 cost calculation example
const EC2CostAnalysis = {
  // t3.micro (1 vCPU, 1GB RAM) - suitable for light APIs
  t3Micro: {
    onDemand: "$0.0104/hour = $7.49/month",
    reserved1Year: "$0.0063/hour = $4.54/month (39% savings)",
    reserved3Years: "$0.0042/hour = $3.03/month (60% savings)",
  },

  // t3.small (2 vCPU, 2GB RAM) - suitable for moderate traffic
  t3Small: {
    onDemand: "$0.0208/hour = $14.98/month",
    reserved1Year: "$0.0126/hour = $9.07/month",
    reserved3Years: "$0.0084/hour = $6.05/month",
  },

  additionalCosts: {
    loadBalancer: "$16.20/month (Application Load Balancer)",
    ebs: "$0.10/GB-month for GP3 storage",
    dataTransfer: "$0.09/GB for first 10TB out to internet",
  },
};

// Total monthly cost example for production API
const ProductionCostExample = {
  instances: "2 x t3.small (for high availability) = $29.96",
  loadBalancer: "$16.20",
  storage: "20GB x 2 instances = $4.00",
  dataTransfer: "100GB/month = $9.00",
  total: "$59.16/month",
};

AWS ECS: Container Orchestration

Overview

Amazon ECS (Elastic Container Service) is a fully managed container orchestration service that eliminates the need to install and operate your own container management software. It integrates seamlessly with other AWS services and supports both EC2 and Fargate launch types.

Pros and Cons

Advantages

  • No Server Management: AWS handles the underlying infrastructure
  • Container Benefits: Consistent deployments across environments
  • Auto Scaling: Automatic scaling based on metrics
  • Service Discovery: Built-in service discovery and load balancing
  • Blue-Green Deployments: Zero-downtime deployment strategies
  • Resource Efficiency: Better resource utilization than traditional VMs

Disadvantages

  • Container Learning Curve: Requires Docker and containerization knowledge
  • Complexity: More complex than serverless but simpler than raw EC2
  • Cold Start (Fargate): Initial container startup time
  • Cost for Light Traffic: May be expensive for very low traffic applications

Implementation Example

Dockerfile for Node.js API

# Dockerfile - Multi-stage build for production
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install all dependencies (including devDependencies)
RUN npm ci

# Copy source code
COPY . .

# Build if needed (uncomment for TypeScript projects)
# RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create app directory with proper permissions
RUN mkdir -p /home/node/app && chown -R node:node /home/node/app

# Switch to non-root user
USER node
WORKDIR /home/node/app

# Copy package files and install production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy built application from builder stage
COPY --chown=node:node --from=builder /app .

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# Expose port
EXPOSE 3000

# Use dumb-init for proper signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]

Health Check Script

// healthcheck.js - Health check script for Docker
const http = require("http");

const options = {
  hostname: "localhost",
  port: 3000,
  path: "/health",
  method: "GET",
};

const healthCheck = http.request(options, (res) => {
  if (res.statusCode === 200) {
    process.exit(0);
  } else {
    process.exit(1);
  }
});

healthCheck.on("error", () => {
  process.exit(1);
});

healthCheck.end();

ECS Task Definition (CloudFormation)

# ecs-task-definition.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Node.js API ECS Task Definition"

Parameters:
  ImageUri:
    Type: String
    Description: ECR image URI
  Environment:
    Type: String
    Default: production

Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub "${AWS::StackName}-nodejs-api"
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      Cpu: 512
      Memory: 1024
      ExecutionRoleArn: !Ref ExecutionRole
      TaskRoleArn: !Ref TaskRole
      ContainerDefinitions:
        - Name: nodejs-api
          Image: !Ref ImageUri
          Essential: true
          PortMappings:
            - ContainerPort: 3000
              Protocol: tcp
          Environment:
            - Name: NODE_ENV
              Value: !Ref Environment
            - Name: PORT
              Value: "3000"
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref CloudWatchLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: ecs
          HealthCheck:
            Command:
              - CMD-SHELL
              - "node healthcheck.js"
            Interval: 30
            Timeout: 5
            Retries: 3
            StartPeriod: 60

  CloudWatchLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/ecs/${AWS::StackName}-nodejs-api"
      RetentionInDays: 7

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: TaskRolePolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt CloudWatchLogGroup.Arn

ECS Service with Auto Scaling

# ecs-service.yml
Resources:
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: LoadBalancerListener
    Properties:
      ServiceName: !Sub "${AWS::StackName}-nodejs-api"
      Cluster: !Ref ECSCluster
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 50
        DeploymentCircuitBreaker:
          Enable: true
          Rollback: true
      DesiredCount: 2
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !Ref ContainerSecurityGroup
          Subnets:
            - !Ref PrivateSubnet1
            - !Ref PrivateSubnet2
          AssignPublicIp: DISABLED
      TaskDefinition: !Ref TaskDefinition
      LoadBalancers:
        - ContainerName: nodejs-api
          ContainerPort: 3000
          TargetGroupArn: !Ref TargetGroup

  # Auto Scaling Target
  ScalableTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      ServiceNamespace: ecs
      ScalableDimension: ecs:service:DesiredCount
      ResourceId: !Sub "service/${ECSCluster}/${ECSService.Name}"
      MinCapacity: 1
      MaxCapacity: 10
      RoleARN: !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService"

  # Auto Scaling Policy
  ScalingPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub "${AWS::StackName}-scaling-policy"
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref ScalableTarget
      TargetTrackingScalingPolicyConfiguration:
        PredefinedMetricSpecification:
          PredefinedMetricType: ECSServiceAverageCPUUtilization
        TargetValue: 50
        ScaleOutCooldown: 300
        ScaleInCooldown: 300

Cost Analysis for ECS

// ECS Fargate cost calculation
const ECSCostAnalysis = {
  // Fargate pricing (us-east-1)
  fargateCompute: {
    cpu: "$0.04048 per vCPU per hour",
    memory: "$0.004445 per GB per hour",
  },

  // Example: 0.5 vCPU, 1GB RAM
  smallTask: {
    hourly: "($0.04048 * 0.5) + ($0.004445 * 1) = $0.024685/hour",
    monthly: "$17.77/month per task",
    production: "2 tasks for HA = $35.54/month",
  },

  // Example: 1 vCPU, 2GB RAM
  mediumTask: {
    hourly: "($0.04048 * 1) + ($0.004445 * 2) = $0.04937/hour",
    monthly: "$35.55/month per task",
    production: "2 tasks for HA = $71.10/month",
  },

  additionalCosts: {
    applicationLoadBalancer: "$16.20/month",
    natGateway: "$32.40/month (for private subnets)",
    cloudWatchLogs: "$0.50/GB ingested",
    ecrStorage: "$0.10/GB-month",
  },
};

// Total production cost example
const ECSProductionCost = {
  compute: "2 x medium tasks = $71.10",
  loadBalancer: "$16.20",
  natGateway: "$32.40",
  logs: "10GB/month = $5.00",
  ecr: "1GB = $0.10",
  total: "$124.80/month",
};

AWS Lambda: Serverless Functions

Overview

AWS Lambda is a serverless computing service that runs your code in response to events without managing servers. It automatically scales based on incoming requests and you only pay for the compute time you consume.

Pros and Cons

Advantages

  • Zero Server Management: No infrastructure to provision or manage
  • Automatic Scaling: Scales from zero to thousands of concurrent executions
  • Pay-per-Use: Only pay for actual execution time
  • Fast Deployment: Quick deployments and updates
  • Event-Driven: Perfect integration with other AWS services
  • Cost-Effective: Extremely cost-effective for variable or light traffic

Disadvantages

  • Cold Starts: Initial latency when functions haven't been used recently
  • Execution Time Limits: Maximum 15-minute execution time
  • Memory Limitations: Maximum 10,240 MB memory allocation
  • Vendor Lock-in: Tightly coupled to AWS ecosystem
  • Debugging Complexity: More challenging to debug and monitor

Implementation Example

Express.js API with Serverless Framework

// handler.js - Lambda function handler
const serverless = require("serverless-http");
const express = require("express");
const cors = require("cors");

const app = express();

// Middleware
app.use(cors());
app.use(express.json());

// Health check
app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString(),
    version: process.env.VERSION || "1.0.0",
  });
});

// API routes
app.get("/api/users", async (req, res) => {
  try {
    // Your business logic here
    const users = await getUsersFromDatabase();
    res.json({ users });
  } catch (error) {
    console.error("Error fetching users:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});

app.post("/api/users", async (req, res) => {
  try {
    const userData = req.body;
    const newUser = await createUser(userData);
    res.status(201).json(newUser);
  } catch (error) {
    console.error("Error creating user:", error);
    res.status(500).json({ error: "Failed to create user" });
  }
});

// Global error handler
app.use((error, req, res, next) => {
  console.error("Unhandled error:", error);
  res.status(500).json({ error: "Internal server error" });
});

// Export the handler
module.exports.handler = serverless(app);

// Mock functions for example
async function getUsersFromDatabase() {
  // In real implementation, connect to database
  return [
    { id: 1, name: "John Doe", email: "john@example.com" },
    { id: 2, name: "Jane Smith", email: "jane@example.com" },
  ];
}

async function createUser(userData) {
  // In real implementation, save to database
  return { id: Date.now(), ...userData, createdAt: new Date() };
}

Serverless Framework Configuration

# serverless.yml - Serverless Framework configuration
service: nodejs-api-lambda

frameworkVersion: "3"

provider:
  name: aws
  runtime: nodejs18.x
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}

  # Environment variables
  environment:
    VERSION: ${self:custom.version}
    STAGE: ${self:provider.stage}

  # IAM permissions
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/*"

  # VPC configuration (if needed)
  # vpc:
  #   securityGroupIds:
  #     - sg-xxxxxxxxx
  #   subnetIds:
  #     - subnet-xxxxxxxxx
  #     - subnet-yyyyyyyyy

custom:
  version: ${file(package.json):version}

  # Serverless plugins
  serverless-offline:
    httpPort: 3000
    host: 0.0.0.0

functions:
  api:
    handler: handler.handler
    timeout: 30
    memorySize: 256
    reservedConcurrency: 50 # Limit concurrent executions
    events:
      - http:
          path: /{proxy+}
          method: ANY
          cors: true
      - http:
          path: /
          method: ANY
          cors: true

    # Provisioned concurrency to reduce cold starts
    provisionedConcurrency: 2

plugins:
  - serverless-offline

# CloudFormation resources
resources:
  Resources:
    # DynamoDB table example
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-${self:provider.stage}-users
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST

    # CloudWatch Log Group
    ApiLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        LogGroupName: /aws/lambda/${self:service}-${self:provider.stage}-api
        RetentionInDays: 7

Lambda-Optimized Express Setup

// lambda-optimized.js - Optimized Lambda handler
const serverless = require("serverless-http");
const express = require("express");

// Create Express app outside the handler (for container reuse)
const app = express();

// Middleware
app.use(express.json({ limit: "1mb" }));

// Connection pooling for database (reuse connections)
let dbConnection = null;

async function getDbConnection() {
  if (!dbConnection) {
    // Initialize database connection
    dbConnection = await createDatabaseConnection();
  }
  return dbConnection;
}

// API routes
app.get("/api/users", async (req, res) => {
  try {
    const db = await getDbConnection();
    const users = await db.collection("users").find({}).toArray();

    res.json({
      users,
      requestId: req.headers["x-request-id"],
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    console.error("Error:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});

// Health check with Lambda context info
app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    memory: {
      used: process.memoryUsage(),
      limit: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE + "MB",
    },
    runtime: process.env.AWS_EXECUTION_ENV,
    version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
  });
});

// Wrap with serverless-http
const handler = serverless(app, {
  binary: ["image/*", "font/*"],
});

// Export handler with error handling
module.exports.handler = async (event, context) => {
  // Prevent Lambda from waiting for empty event loop
  context.callbackWaitsForEmptyEventLoop = false;

  try {
    return await handler(event, context);
  } catch (error) {
    console.error("Lambda handler error:", error);
    return {
      statusCode: 500,
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        error: "Internal server error",
        requestId: context.awsRequestId,
      }),
    };
  }
};

// Mock database connection function
async function createDatabaseConnection() {
  // In real implementation, connect to MongoDB, PostgreSQL, etc.
  return {
    collection: () => ({
      find: () => ({
        toArray: async () => [
          { id: 1, name: "John Doe" },
          { id: 2, name: "Jane Smith" },
        ],
      }),
    }),
  };
}

Cost Analysis for Lambda

// Lambda pricing calculation
const LambdaCostAnalysis = {
  // AWS Lambda pricing (us-east-1)
  pricing: {
    requests: "$0.20 per 1M requests",
    duration: "$0.0000166667 per GB-second",
  },

  // Example: 256MB memory, 200ms average execution
  lightTraffic: {
    requests: "100,000/month",
    memory: "256MB (0.25GB)",
    duration: "200ms (0.2 seconds)",

    requestCost: "$0.02 (100K * $0.20/1M)",
    durationCost: "$0.83 (100K * 0.25GB * 0.2s * $0.0000166667)",
    total: "$0.85/month",
  },

  // Example: 512MB memory, 500ms average execution
  moderateTraffic: {
    requests: "1,000,000/month",
    memory: "512MB (0.5GB)",
    duration: "500ms (0.5 seconds)",

    requestCost: "$0.20 (1M * $0.20/1M)",
    durationCost: "$41.67 (1M * 0.5GB * 0.5s * $0.0000166667)",
    total: "$41.87/month",
  },

  // Cold start mitigation
  provisionedConcurrency: {
    cost: "$0.0000097222 per GB-second",
    example: "2 instances * 512MB * 24/7 = $64.58/month",
    benefit: "Eliminates cold starts for critical functions",
  },
};

// Break-even analysis
const LambdaBreakEven = {
  vsEC2: "Lambda becomes more expensive around 2-3M requests/month",
  vsECS: "Lambda cost-effective up to 1-2M requests/month",
  recommendation: "Use Lambda for variable/unpredictable traffic patterns",
};

Detailed Comparison Matrix

Feature Comparison

const FeatureComparison = {
  scalability: {
    ec2: "Manual/Auto Scaling Groups - Minutes to scale",
    ecs: "Service Auto Scaling - Seconds to minutes",
    lambda: "Automatic - Milliseconds to scale",
  },

  coldStart: {
    ec2: "None - Always warm",
    ecs: "Container startup - 30-60 seconds",
    lambda: "Function initialization - 100ms-5s",
  },

  costModel: {
    ec2: "Pay for running instances 24/7",
    ecs: "Pay for allocated CPU/memory",
    lambda: "Pay per request and execution time",
  },

  maintenance: {
    ec2: "OS patches, security updates, monitoring",
    ecs: "Container updates, task management",
    lambda: "Code updates only",
  },

  monitoring: {
    ec2: "CloudWatch + custom metrics",
    ecs: "Built-in CloudWatch integration",
    lambda: "Automatic CloudWatch integration",
  },

  deployment: {
    ec2: "Blue-green, rolling, or in-place updates",
    ecs: "Rolling updates with health checks",
    lambda: "Aliases and weighted routing",
  },
};

Use Case Matrix

const UseCaseRecommendations = {
  highTrafficSteady: {
    recommendation: "EC2 with Reserved Instances",
    reasoning: "Predictable costs, better performance per dollar",
    example: "E-commerce API with consistent traffic",
  },

  moderateTrafficVariable: {
    recommendation: "ECS Fargate",
    reasoning: "Good balance of cost, scalability, and management",
    example: "Business application with office hours traffic",
  },

  lightTrafficSpiky: {
    recommendation: "Lambda",
    reasoning: "Pay only for actual usage, automatic scaling",
    example: "Webhook processors, periodic reports",
  },

  longRunningProcesses: {
    recommendation: "EC2 or ECS",
    reasoning: "Lambda has 15-minute execution limit",
    example: "Data processing pipelines, real-time services",
  },

  microservices: {
    recommendation: "Lambda or ECS",
    reasoning: "Event-driven architecture, independent scaling",
    example: "Order processing, notification services",
  },
};

Performance Considerations

Latency Comparison

// Typical latency characteristics
const LatencyComparison = {
  ec2: {
    coldStart: "0ms (always warm)",
    apiResponse: "10-50ms (depends on optimization)",
    pros: "Consistent low latency",
    cons: "Resource overhead when idle",
  },

  ecs: {
    coldStart: "30-60s (container startup)",
    apiResponse: "10-100ms (similar to EC2 when warm)",
    pros: "Good performance when scaled",
    cons: "Container startup delay",
  },

  lambda: {
    coldStart: "100ms-5s (depends on memory/dependencies)",
    apiResponse: "50-200ms (including cold start penalty)",
    pros: "No idle resource consumption",
    cons: "Variable latency due to cold starts",
  },
};

// Cold start optimization strategies
const ColdStartOptimization = {
  lambda: [
    "Use provisioned concurrency for critical functions",
    "Minimize package size and dependencies",
    "Use connection pooling and caching",
    "Optimize initialization code",
    "Choose appropriate memory allocation",
  ],

  ecs: [
    "Use smaller, optimized base images",
    "Implement proper health checks",
    "Pre-warm containers with traffic",
    "Use spot instances for cost optimization",
  ],
};

Security Considerations

Security Comparison

const SecurityConsiderations = {
  ec2: {
    responsibilities: [
      "OS security patches and updates",
      "Network security groups configuration",
      "SSH key management",
      "Application-level security",
      "Log management and monitoring",
    ],
    benefits: [
      "Full control over security configuration",
      "Custom security tools installation",
      "Compliance requirements easier to meet",
    ],
  },

  ecs: {
    responsibilities: [
      "Container security scanning",
      "Task role permissions",
      "Network configuration",
      "Application security",
    ],
    benefits: [
      "AWS manages underlying infrastructure security",
      "Built-in secrets management",
      "IAM integration for fine-grained permissions",
    ],
  },

  lambda: {
    responsibilities: [
      "Function code security",
      "IAM role permissions",
      "Environment variables encryption",
    ],
    benefits: [
      "Minimal attack surface",
      "AWS manages all infrastructure security",
      "Automatic security patches",
      "Built-in DDoS protection",
    ],
  },
};

Best Practices by Platform

EC2 Security Best Practices

// EC2 security configuration example
const EC2SecurityConfig = {
  networkSecurity: {
    securityGroups: [
      "Allow HTTPS (443) from ALB only",
      "Allow SSH (22) from bastion host only",
      "Restrict outbound to necessary services",
    ],
    vpc: "Deploy in private subnets with NAT Gateway",
  },

  instanceSecurity: {
    patches: "Automated patching with Systems Manager",
    monitoring: "CloudWatch agent + AWS Config",
    access: "Session Manager instead of SSH keys",
  },

  applicationSecurity: {
    secrets: "AWS Secrets Manager or Parameter Store",
    encryption: "Encrypt data at rest and in transit",
    logging: "Centralized logging with CloudWatch",
  },
};

ECS Security Best Practices

# Secure Dockerfile example
FROM node:18-alpine

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001

# Install security updates
RUN apk update && apk upgrade

# Set working directory
WORKDIR /app

# Copy and install dependencies as root
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy app files and change ownership
COPY --chown=nodeuser:nodejs . .

# Switch to non-root user
USER nodeuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js || exit 1

EXPOSE 3000
CMD ["node", "server.js"]

Migration Strategies

EC2 to Lambda Migration

// Migration strategy for EC2 to Lambda
const EC2ToLambdaMigration = {
  assessment: [
    "Identify stateless API endpoints",
    "Measure current request patterns",
    "Check execution time limits (15 min max)",
    "Review external dependencies",
  ],

  approach: {
    strangler: "Gradually migrate endpoints one by one",
    parallel: "Run both systems with traffic splitting",
    bigBang: "Complete migration during maintenance window",
  },

  considerations: [
    "Database connection pooling changes",
    "Session storage (move to DynamoDB/Redis)",
    "File uploads (use S3 pre-signed URLs)",
    "Long-running processes (consider Step Functions)",
  ],
};

// Example migration code
const MigrationExample = {
  before: `
    // Traditional Express.js on EC2
    app.post('/api/process', (req, res) => {
      // Long-running process
      processLargeFile(req.body.fileUrl, (result) => {
        res.json(result);
      });
    });
  `,

  after: `
    // Lambda with Step Functions for long processes
    app.post('/api/process', async (req, res) => {
      const stepFunctions = new AWS.StepFunctions();
      
      const execution = await stepFunctions.startExecution({
        stateMachineArn: process.env.PROCESS_STATE_MACHINE,
        input: JSON.stringify({ fileUrl: req.body.fileUrl })
      }).promise();
      
      res.json({ 
        processId: execution.executionArn,
        status: 'started' 
      });
    });
  `,
};

Lambda to ECS Migration

const LambdaToECSMigration = {
  reasons: [
    "Exceeded 15-minute execution limit",
    "Cold start latency too high",
    "Need for persistent connections",
    "Cost optimization for high traffic",
  ],

  strategy: {
    containerization: "Package Lambda code in Docker",
    infrastructure: "Set up ECS cluster and services",
    monitoring: "Migrate CloudWatch metrics and alarms",
    deployment: "Blue-green deployment setup",
  },

  codeChanges: `
    // Minimal changes needed - mostly configuration
    // Lambda handler.js becomes:
    const express = require('express');
    const app = express();
    
    // Your existing Lambda logic as Express routes
    app.post('/api/endpoint', async (req, res) => {
      // Same business logic from Lambda
      const result = await processRequest(req.body);
      res.json(result);
    });
    
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
      console.log('Server running on port', PORT);
    });
  `,
};

Monitoring and Observability

Monitoring Strategies by Platform

const MonitoringStrategies = {
  ec2: {
    metrics: [
      "CPU utilization and memory usage",
      "Network I/O and disk utilization",
      "Application-specific metrics",
      "Custom business metrics",
    ],

    tools: [
      "CloudWatch Agent for system metrics",
      "Application Performance Monitoring (APM)",
      "Custom metrics via CloudWatch API",
      "Log aggregation (ELK stack or CloudWatch Logs)",
    ],

    alerts: [
      "High CPU/memory usage",
      "Disk space warnings",
      "Application error rates",
      "Response time degradation",
    ],
  },

  ecs: {
    metrics: [
      "Container CPU and memory utilization",
      "Service scaling metrics",
      "Task health and deployment status",
      "Load balancer metrics",
    ],

    tools: [
      "Built-in CloudWatch Container Insights",
      "AWS X-Ray for distributed tracing",
      "Third-party monitoring (DataDog, New Relic)",
      "Custom metrics from application",
    ],

    alerts: [
      "Service deployment failures",
      "Container restart loops",
      "High resource utilization",
      "Load balancer health checks failing",
    ],
  },

  lambda: {
    metrics: [
      "Invocation count and duration",
      "Error rate and throttles",
      "Cold start frequency",
      "Concurrent executions",
    ],

    tools: [
      "Built-in CloudWatch metrics",
      "AWS X-Ray for request tracing",
      "Lambda Insights for detailed monitoring",
      "Third-party serverless monitoring tools",
    ],

    alerts: [
      "High error rates",
      "Duration approaching timeout",
      "Throttling occurrences",
      "Memory usage warnings",
    ],
  },
};

Sample Monitoring Dashboard

// CloudWatch Dashboard configuration
const MonitoringDashboard = {
  widgets: [
    {
      type: "metric",
      properties: {
        metrics: [
          ["AWS/Lambda", "Invocations", "FunctionName", "nodejs-api"],
          [".", "Errors", ".", "."],
          [".", "Duration", ".", "."],
          [".", "Throttles", ".", "."],
        ],
        period: 300,
        stat: "Sum",
        region: "us-east-1",
        title: "Lambda Performance Metrics",
      },
    },

    {
      type: "log",
      properties: {
        query: `
          SOURCE '/aws/lambda/nodejs-api'
          | fields @timestamp, @message
          | filter @message like /ERROR/
          | sort @timestamp desc
          | limit 50
        `,
        region: "us-east-1",
        title: "Recent Errors",
      },
    },
  ],
};

Cost Optimization Strategies

Platform-Specific Cost Optimization

const CostOptimization = {
  ec2: {
    strategies: [
      "Use Reserved Instances for predictable workloads",
      "Implement auto-scaling to match demand",
      "Use Spot Instances for fault-tolerant workloads",
      "Right-size instances based on monitoring",
      "Schedule instances for non-production environments",
    ],

    example: {
      scenario: "API with predictable business hours traffic",
      implementation: "Auto Scaling Group with scheduled scaling",
      savings: "40-60% cost reduction compared to always-on",
    },
  },

  ecs: {
    strategies: [
      "Use Fargate Spot for cost-sensitive workloads",
      "Optimize container resource allocation",
      "Implement auto-scaling policies",
      "Use ARM-based Graviton processors",
      "Schedule services for development environments",
    ],

    example: {
      scenario: "Containerized microservices with variable load",
      implementation: "ECS Service Auto Scaling with Fargate Spot",
      savings: "50-70% reduction with Spot pricing",
    },
  },

  lambda: {
    strategies: [
      "Optimize memory allocation for cost/performance",
      "Use provisioned concurrency judiciously",
      "Minimize package size and cold starts",
      "Implement efficient error handling",
      "Use ARM-based Lambda functions",
    ],

    example: {
      scenario: "Event-driven API with sporadic traffic",
      implementation: "Right-sized memory with ARM runtime",
      savings: "20% with ARM + optimal memory allocation",
    },
  },
};

Deployment Automation

CI/CD Pipeline Examples

GitHub Actions for Lambda Deployment

# .github/workflows/lambda-deploy.yml
name: Deploy Lambda API

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  AWS_REGION: us-east-1
  NODE_VERSION: "18"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Run linting
        run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        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: Install Serverless Framework
        run: npm install -g serverless

      - name: Deploy to AWS Lambda
        run: |
          serverless deploy --stage production --verbose

      - name: Run smoke tests
        run: |
          npm run test:smoke -- --url ${{ steps.deploy.outputs.api-url }}

Docker Build for ECS

# .github/workflows/ecs-deploy.yml
name: Deploy to ECS

on:
  push:
    branches: [main]

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: nodejs-api
  ECS_SERVICE: nodejs-api-service
  ECS_CLUSTER: nodejs-api-cluster

jobs:
  deploy:
    name: Deploy to ECS
    runs-on: ubuntu-latest

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

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        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: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

      - name: Download task definition
        run: |
          aws ecs describe-task-definition \
            --task-definition $ECS_SERVICE \
            --query taskDefinition > task-definition.json

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: nodejs-api
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

Conclusion and Recommendations

Decision Framework

Use this decision framework to choose the right deployment option:

const DecisionFramework = {
  questions: [
    {
      question: "What's your traffic pattern?",
      answers: {
        predictableHigh: "EC2 with Reserved Instances",
        variable: "ECS Fargate with Auto Scaling",
        sporadic: "Lambda",
        unknown: "Start with Lambda, migrate if needed",
      },
    },

    {
      question: "What's your team's expertise?",
      answers: {
        infrastructure: "EC2 - full control",
        containers: "ECS - containerization benefits",
        development: "Lambda - focus on code",
      },
    },

    {
      question: "What are your latency requirements?",
      answers: {
        ultraLow: "EC2 - consistent performance",
        moderate: "ECS - good balance",
        flexible: "Lambda - cost over latency",
      },
    },

    {
      question: "What's your budget constraint?",
      answers: {
        optimizeForCost: "Lambda for variable loads",
        predictableBudget: "EC2 Reserved Instances",
        balanced: "ECS Fargate",
      },
    },
  ],
};

Final Recommendations

Start Small, Scale Smart

const ScalingPath = {
  phase1: {
    option: "Lambda",
    reasoning: "Quick to deploy, low initial cost, learn requirements",
    duration: "3-6 months",
  },

  phase2: {
    option: "ECS or stick with Lambda",
    reasoning: "Based on traffic patterns and cost analysis",
    decision:
      "If >1M requests/month or need persistent connections, move to ECS",
  },

  phase3: {
    option: "EC2 for high-scale predictable workloads",
    reasoning: "Cost optimization for steady high traffic",
    threshold: "Reserved capacity becomes cost-effective",
  },
};

Best Practices Summary

  1. Start with Lambda for new projects to understand requirements
  2. Monitor costs and performance from day one
  3. Design for portability - avoid vendor lock-in where possible
  4. Implement proper monitoring regardless of platform choice
  5. Use Infrastructure as Code for reproducible deployments
  6. Plan for disaster recovery and multi-region deployments

Each deployment option has its sweet spot. Lambda excels for variable traffic and rapid development, ECS provides excellent balance for most applications, and EC2 offers maximum control and cost efficiency for predictable high-traffic scenarios.

The key is to start simple, measure everything, and evolve your architecture based on actual usage patterns and business requirements rather than premature optimization.