Overview

The SG Cars Trends API is deployed using SST (Serverless Stack) on AWS with infrastructure as code. This guide covers deployment strategies, environments, and best practices.

Infrastructure Architecture

AWS Services Used

AWS Lambda

Serverless functions for API endpoints and workflows

Amazon RDS

PostgreSQL database for data storage

CloudFront

CDN for API distribution and caching

Route 53

DNS management and domain routing

API Gateway

HTTP API management and routing

Systems Manager

Secure parameter storage for secrets

Architecture Diagram

Deployment Environments

Environment Overview

EnvironmentPurposeDomainDatabase
DevelopmentLocal developmentlocalhost:3000Local PostgreSQL
StagingPre-production testingapi-staging.sgcarstrends.comAWS RDS (staging)
ProductionLive APIapi.sgcarstrends.comAWS RDS (production)

Environment Configuration

import { SSTConfig } from 'sst';
import { API } from './stacks/API';

export default {
  config(_input) {
    return {
      name: 'sgcarstrends-api',
      region: 'ap-southeast-1', // Singapore
      profile: _input.stage === 'prod' ? 'production' : 'default',
    };
  },
  stacks(app) {
    app.stack(API);
  },
} satisfies SSTConfig;

Prerequisites

1. AWS Account Setup

1

AWS Account

Create an AWS account and configure billing

2

IAM User

Create an IAM user with necessary permissions

3

AWS CLI

Install and configure AWS CLI

aws configure
4

Domain Setup

Configure domain in Route 53 (optional)

2. Required Permissions

The deployment requires the following AWS permissions:

3. Environment Variables

Set up deployment-specific environment variables:

# AWS Configuration
AWS_PROFILE=production
AWS_REGION=ap-southeast-1

# Database
DATABASE_URL=postgresql://user:pass@prod-db.region.rds.amazonaws.com:5432/sgcarstrends

# External Services
UPSTASH_REDIS_REST_URL=https://prod-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=prod_token_here
QSTASH_URL=https://qstash.upstash.io
QSTASH_TOKEN=prod_qstash_token

# API Keys
SG_CARS_TRENDS_API_TOKEN=prod_api_token
LTA_DATAMALL_API_KEY=lta_api_key

# Social Media (Production)
LINKEDIN_ACCESS_TOKEN=prod_linkedin_token
TWITTER_BEARER_TOKEN=prod_twitter_token
DISCORD_WEBHOOK_URL=prod_discord_webhook

Deployment Process

1. Initial Deployment

# Build and deploy to staging
pnpm -F @sgcarstrends/api deploy --stage staging

2. Database Migration

# Connect to production database
pnpm -F @sgcarstrends/api migrate --stage prod

# Check migration status
pnpm -F @sgcarstrends/api migrate:check --stage prod

3. Verification

After deployment, verify the API:

curl https://api.sgcarstrends.com/health

SST Stack Configuration

API Stack

import { StackContext, Api, Config } from 'sst/constructs';

export function API({ stack }: StackContext) {
  // Environment variables
  const DATABASE_URL = new Config.Secret(stack, 'DATABASE_URL');
  const API_TOKEN = new Config.Secret(stack, 'SG_CARS_TRENDS_API_TOKEN');
  const REDIS_URL = new Config.Secret(stack, 'UPSTASH_REDIS_REST_URL');
  const REDIS_TOKEN = new Config.Secret(stack, 'UPSTASH_REDIS_REST_TOKEN');

  // API Gateway
  const api = new Api(stack, 'Api', {
    runtime: 'nodejs18.x',
    architecture: 'arm64',
    timeout: '30 seconds',
    memorySize: '1024 MB',
    environment: {
      NODE_ENV: stack.stage === 'prod' ? 'production' : 'development',
    },
    bind: [DATABASE_URL, API_TOKEN, REDIS_URL, REDIS_TOKEN],
    routes: {
      'ANY /': 'src/index.handler',
    },
    customDomain: {
      domainName: stack.stage === 'prod' 
        ? 'api.sgcarstrends.com' 
        : `api-${stack.stage}.sgcarstrends.com`,
      hostedZone: 'sgcarstrends.com',
    },
  });

  // Output API URL
  stack.addOutputs({
    ApiEndpoint: api.url,
    CustomDomain: api.customDomainUrl,
  });

  return { api };
}

Database Stack (Optional)

import { StackContext, RDS } from 'sst/constructs';

export function Database({ stack }: StackContext) {
  const rds = new RDS(stack, 'Database', {
    engine: 'postgresql14.9',
    defaultDatabaseName: 'sgcarstrends',
    migrations: 'migrations',
    scaling: {
      autoPause: stack.stage !== 'prod',
      minCapacity: 'ACU_2',
      maxCapacity: stack.stage === 'prod' ? 'ACU_16' : 'ACU_8',
    },
  });

  return { rds };
}

CI/CD Pipeline

GitHub Actions Workflow

name: Deploy

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: sgcarstrends_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          
      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - name: Install dependencies
        run: pnpm install
        
      - name: Run tests
        run: pnpm test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sgcarstrends_test
          
      - name: Run linting
        run: pnpm lint

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          
      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - name: Install dependencies
        run: pnpm install
        
      - 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: ap-southeast-1
          
      - name: Deploy to staging
        run: pnpm -F @sgcarstrends/api deploy --stage staging
        env:
          DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
          SG_CARS_TRENDS_API_TOKEN: ${{ secrets.STAGING_API_TOKEN }}

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          
      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - name: Install dependencies
        run: pnpm install
        
      - 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: ap-southeast-1
          
      - name: Deploy to production
        run: pnpm -F @sgcarstrends/api deploy --stage prod
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
          SG_CARS_TRENDS_API_TOKEN: ${{ secrets.PROD_API_TOKEN }}

Deployment Scripts

#!/bin/bash

# Deployment script with safety checks
set -e

STAGE=${1:-staging}

echo "🚀 Deploying to $STAGE environment..."

# Run tests first
echo "🧪 Running tests..."
pnpm test

# Run linting
echo "🔍 Running linting..."
pnpm lint

# Check for migrations
echo "📊 Checking migrations..."
pnpm -F @sgcarstrends/api migrate:check --stage $STAGE

# Deploy application
echo "🌍 Deploying application..."
pnpm -F @sgcarstrends/api deploy --stage $STAGE

# Verify deployment
echo "✅ Verifying deployment..."
if [ "$STAGE" = "prod" ]; then
  curl -f https://api.sgcarstrends.com/health
else
  curl -f https://api-$STAGE.sgcarstrends.com/health
fi

echo "✨ Deployment to $STAGE completed successfully!"

Production Configuration

Performance Optimization

export const lambdaConfig = {
  runtime: 'nodejs18.x',
  architecture: 'arm64', // Better price/performance
  timeout: '30 seconds',
  memorySize: '1024 MB', // Adjust based on needs
  environment: {
    NODE_ENV: 'production',
    NODE_OPTIONS: '--enable-source-maps',
  },
  bundling: {
    minify: true,
    sourcemap: true,
    target: 'es2022',
    format: 'esm',
  },
};

Database Configuration

export const rdsConfig = {
  engine: 'postgresql14.9',
  instanceType: 'db.t4g.micro', // Start small, scale up
  storage: {
    type: 'gp3',
    size: 20, // GB
    encrypted: true,
  },
  backup: {
    retentionPeriod: 7, // days
    deleteAutomatedBackups: false,
  },
  monitoring: {
    enablePerformanceInsights: true,
    performanceInsightsRetentionPeriod: 7,
  },
};

Security Configuration

export const securityConfig = {
  // API Gateway
  throttling: {
    rateLimit: 1000,
    burstLimit: 2000,
  },
  
  // CORS
  cors: {
    allowOrigins: ['https://sgcarstrends.com'],
    allowMethods: ['GET', 'POST'],
    allowHeaders: ['Authorization', 'Content-Type'],
  },
  
  // SSL/TLS
  certificate: {
    domainName: 'sgcarstrends.com',
    validation: Certificate.Validation.fromDns(),
  },
};

Monitoring and Logging

CloudWatch Configuration

import { Alarm, Metric } from 'aws-cdk-lib/aws-cloudwatch';

// Lambda error rate alarm
const errorAlarm = new Alarm(this, 'ApiErrorAlarm', {
  metric: lambdaFunction.metricErrors(),
  threshold: 10,
  evaluationPeriods: 2,
  alarmDescription: 'API Lambda function error rate too high',
});

// Database connection alarm
const dbAlarm = new Alarm(this, 'DatabaseAlarm', {
  metric: new Metric({
    namespace: 'AWS/RDS',
    metricName: 'DatabaseConnections',
    dimensionsMap: {
      DBInstanceIdentifier: database.instanceIdentifier,
    },
  }),
  threshold: 80,
  evaluationPeriods: 3,
});

Application Logging

import { Logger } from '@aws-lambda-powertools/logger';

const logger = new Logger({
  serviceName: 'sgcarstrends-api',
  logLevel: process.env.LOG_LEVEL || 'INFO',
  environment: process.env.NODE_ENV,
});

// Usage in handlers
export const handler = async (event: APIGatewayProxyEvent) => {
  logger.info('Request received', { 
    path: event.path,
    method: event.httpMethod 
  });
  
  try {
    // Handler logic
    const result = await processRequest(event);
    
    logger.info('Request completed', { 
      statusCode: result.statusCode 
    });
    
    return result;
  } catch (error) {
    logger.error('Request failed', { 
      error: error.message,
      stack: error.stack 
    });
    
    throw error;
  }
};

Rollback Procedures

Automated Rollback

# Rollback to previous version
sst deploy --stage prod --rollback

# Rollback to specific version
sst deploy --stage prod --version v1.2.3

Manual Rollback

# 1. Check current deployment
aws cloudformation describe-stacks --stack-name sgcarstrends-api-prod

# 2. List previous versions
aws lambda list-versions-by-function --function-name sgcarstrends-api-prod

# 3. Update alias to previous version
aws lambda update-alias \
  --function-name sgcarstrends-api-prod \
  --name live \
  --function-version 2

# 4. Verify rollback
curl https://api.sgcarstrends.com/health

Troubleshooting

Common Deployment Issues

Debugging Tools

# Check function logs
aws logs tail /aws/lambda/sgcarstrends-api-prod --follow

# Check function metrics
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Invocations \
  --dimensions Name=FunctionName,Value=sgcarstrends-api-prod \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-01T23:59:59Z \
  --period 300 \
  --statistics Sum

# Check API Gateway logs
aws logs describe-log-groups --log-group-name-prefix API-Gateway-Execution-Logs

Cost Optimization

Cost Monitoring

Lambda Optimization

  • Use ARM64 architecture
  • Right-size memory allocation
  • Optimize cold start times
  • Monitor invocation patterns

Database Optimization

  • Use Aurora Serverless for variable workloads
  • Enable auto-pause for staging
  • Optimize queries and indexes
  • Monitor connection usage

Data Transfer

  • Use CloudFront for caching
  • Optimize response sizes
  • Enable compression
  • Monitor bandwidth usage

Storage

  • Use S3 for static assets
  • Implement data lifecycle policies
  • Archive old data
  • Monitor storage usage

Cost Alerts

import { BudgetProps, Budget } from 'aws-cdk-lib/aws-budgets';

const monthlyBudget = new Budget(this, 'MonthlyBudget', {
  budget: {
    budgetName: 'sgcarstrends-monthly-budget',
    budgetLimit: {
      amount: 100, // USD
      unit: 'USD',
    },
    timeUnit: 'MONTHLY',
    budgetType: 'COST',
  },
  notificationsWithSubscribers: [
    {
      notification: {
        comparisonOperator: 'GREATER_THAN',
        threshold: 80, // 80% of budget
        thresholdType: 'PERCENTAGE',
        notificationType: 'ACTUAL',
      },
      subscribers: [
        {
          address: 'alerts@sgcarstrends.com',
          subscriptionType: 'EMAIL',
        },
      ],
    },
  ],
});

Next Steps