Agent skill

deploying-to-aws

Specialized skill for deploying Next.js applications to AWS using SST (Serverless Stack) or Vercel, with DynamoDB integration, IAM configuration, and infrastructure as code. Use when setting up AWS resources or deploying production applications.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/devops/deploying-to-aws-swapkats-robin

SKILL.md

Deploying to AWS

You are an expert in deploying production-ready Next.js applications to AWS with proper infrastructure, security, and best practices.

Deployment Options

Option 1: SST (Serverless Stack) - Recommended for AWS

  • Full control over AWS resources
  • Infrastructure as Code (IaC)
  • Local development with AWS resources
  • Type-safe infrastructure definitions
  • Built specifically for Next.js + serverless

Option 2: Vercel

  • Simplest deployment
  • Automatic CI/CD
  • Global CDN
  • Can still use AWS DynamoDB
  • Great for getting started quickly

SST Deployment (Recommended)

Installation

bash
npm install --save-dev sst aws-cdk-lib constructs

Project Structure

my-app/
├── sst.config.ts          # SST configuration
├── stacks/
│   ├── Database.ts        # DynamoDB stack
│   ├── Web.ts             # Next.js stack
│   └── Auth.ts            # NextAuth config
├── app/                   # Next.js app
└── lib/
    └── db/
        └── client.ts      # DynamoDB client

SST Configuration

typescript
// sst.config.ts
import { SSTConfig } from 'sst';
import { Database } from './stacks/Database';
import { Web } from './stacks/Web';

export default {
  config(_input) {
    return {
      name: 'my-app',
      region: 'us-east-1',
    };
  },
  stacks(app) {
    app.stack(Database).stack(Web);
  },
} satisfies SSTConfig;

Database Stack

typescript
// stacks/Database.ts
import { StackContext, Table } from 'sst/constructs';

export function Database({ stack }: StackContext) {
  const table = new Table(stack, 'AppTable', {
    fields: {
      PK: 'string',
      SK: 'string',
      GSI1PK: 'string',
      GSI1SK: 'string',
      GSI2PK: 'string',
      GSI2SK: 'string',
    },
    primaryIndex: {
      partitionKey: 'PK',
      sortKey: 'SK',
    },
    globalIndexes: {
      GSI1: {
        partitionKey: 'GSI1PK',
        sortKey: 'GSI1SK',
      },
      GSI2: {
        partitionKey: 'GSI2PK',
        sortKey: 'GSI2SK',
      },
    },
    stream: 'new-and-old-images', // Enable streams for real-time features
  });

  return { table };
}

Web Stack

typescript
// stacks/Web.ts
import { StackContext, NextjsSite, use } from 'sst/constructs';
import { Database } from './Database';

export function Web({ stack }: StackContext) {
  const { table } = use(Database);

  const site = new NextjsSite(stack, 'Site', {
    path: '.',
    environment: {
      DYNAMODB_TABLE_NAME: table.tableName,
      AWS_REGION: stack.region,
    },
    permissions: [table],
  });

  stack.addOutputs({
    SiteUrl: site.url,
    TableName: table.tableName,
  });

  return { site };
}

Package.json Scripts

json
{
  "scripts": {
    "dev": "sst dev next dev",
    "build": "next build",
    "start": "next start",
    "deploy": "sst deploy --stage production",
    "deploy:dev": "sst deploy --stage dev",
    "remove": "sst remove --stage production",
    "console": "sst console"
  }
}

Local Development with SST

bash
# Start local development (creates real AWS resources in dev stage)
npm run dev

SST creates:

  • Local Next.js dev server
  • Real DynamoDB table in AWS (isolated dev stage)
  • Local Lambda functions
  • Live AWS resource binding

Deployment

bash
# Deploy to dev
npm run deploy:dev

# Deploy to production
npm run deploy

Environment Variables

SST binds resources automatically, but for NextAuth and other secrets:

typescript
// stacks/Web.ts
const site = new NextjsSite(stack, 'Site', {
  path: '.',
  environment: {
    DYNAMODB_TABLE_NAME: table.tableName,
    AWS_REGION: stack.region,
    NEXTAUTH_URL: process.env.NEXTAUTH_URL!,
    NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET!,
    GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID!,
    GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET!,
  },
  permissions: [table],
});

Set secrets in AWS:

bash
npx sst secrets set NEXTAUTH_SECRET "your-secret-here" --stage production
npx sst secrets set GOOGLE_CLIENT_ID "your-client-id" --stage production
npx sst secrets set GOOGLE_CLIENT_SECRET "your-client-secret" --stage production

DynamoDB Client Configuration

typescript
// lib/db/client.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({
  region: process.env.AWS_REGION || 'us-east-1',
});

export const docClient = DynamoDBDocumentClient.from(client, {
  marshallOptions: {
    convertEmptyValues: true,
    removeUndefinedValues: true,
    convertClassInstanceToMap: true,
  },
  unmarshallOptions: {
    wrapNumbers: false,
  },
});

export const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME!;

IAM Permissions

SST automatically configures IAM for:

  • Lambda function execution role
  • DynamoDB read/write access
  • CloudWatch logs
  • API Gateway invocation

Custom permissions:

typescript
// stacks/Web.ts
const site = new NextjsSite(stack, 'Site', {
  // ...
  permissions: [
    table,
    'ses:SendEmail', // Additional permissions
    's3:GetObject',
  ],
});

Vercel Deployment

Prerequisites

  • Vercel account
  • AWS account with DynamoDB table
  • IAM user with DynamoDB access

Setup

bash
npm install -g vercel
vercel login

Vercel Configuration

json
// vercel.json
{
  "build": {
    "env": {
      "DYNAMODB_TABLE_NAME": "@dynamodb-table-name",
      "AWS_REGION": "@aws-region",
      "AWS_ACCESS_KEY_ID": "@aws-access-key-id",
      "AWS_SECRET_ACCESS_KEY": "@aws-secret-access-key"
    }
  },
  "env": {
    "DYNAMODB_TABLE_NAME": "@dynamodb-table-name",
    "AWS_REGION": "@aws-region",
    "AWS_ACCESS_KEY_ID": "@aws-access-key-id",
    "AWS_SECRET_ACCESS_KEY": "@aws-secret-access-key",
    "NEXTAUTH_URL": "@nextauth-url",
    "NEXTAUTH_SECRET": "@nextauth-secret",
    "GOOGLE_CLIENT_ID": "@google-client-id",
    "GOOGLE_CLIENT_SECRET": "@google-client-secret"
  }
}

Set Environment Variables

bash
vercel env add DYNAMODB_TABLE_NAME production
vercel env add AWS_REGION production
vercel env add AWS_ACCESS_KEY_ID production
vercel env add AWS_SECRET_ACCESS_KEY production
vercel env add NEXTAUTH_URL production
vercel env add NEXTAUTH_SECRET production
vercel env add GOOGLE_CLIENT_ID production
vercel env add GOOGLE_CLIENT_SECRET production

Create DynamoDB Table (CloudFormation)

yaml
# infrastructure/dynamodb-table.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: DynamoDB table for Next.js app

Resources:
  AppTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: my-app-production
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: PK
          AttributeType: S
        - AttributeName: SK
          AttributeType: S
        - AttributeName: GSI1PK
          AttributeType: S
        - AttributeName: GSI1SK
          AttributeType: S
        - AttributeName: GSI2PK
          AttributeType: S
        - AttributeName: GSI2SK
          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
        - IndexName: GSI2
          KeySchema:
            - AttributeName: GSI2PK
              KeyType: HASH
            - AttributeName: GSI2SK
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      Tags:
        - Key: Environment
          Value: production
        - Key: Application
          Value: my-app

Outputs:
  TableName:
    Value: !Ref AppTable
    Description: DynamoDB table name
  TableArn:
    Value: !GetAtt AppTable.Arn
    Description: DynamoDB table ARN
  TableStreamArn:
    Value: !GetAtt AppTable.StreamArn
    Description: DynamoDB stream ARN

Deploy CloudFormation:

bash
aws cloudformation deploy \
  --template-file infrastructure/dynamodb-table.yaml \
  --stack-name my-app-dynamodb \
  --region us-east-1

Create IAM User for Vercel

yaml
# infrastructure/iam-user.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: IAM user for Vercel deployment

Resources:
  VercelUser:
    Type: AWS::IAM::User
    Properties:
      UserName: vercel-my-app-production
      Policies:
        - PolicyName: DynamoDBAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                  - dynamodb:Query
                  - dynamodb:Scan
                  - dynamodb:BatchGetItem
                  - dynamodb:BatchWriteItem
                  - dynamodb:DescribeTable
                Resource:
                  - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/my-app-production'
                  - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/my-app-production/index/*'

  VercelUserAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName: !Ref VercelUser

Outputs:
  AccessKeyId:
    Value: !Ref VercelUserAccessKey
    Description: Access Key ID (add to Vercel env)
  SecretAccessKey:
    Value: !GetAtt VercelUserAccessKey.SecretAccessKey
    Description: Secret Access Key (add to Vercel env)

Deploy:

bash
aws cloudformation deploy \
  --template-file infrastructure/iam-user.yaml \
  --stack-name my-app-iam \
  --capabilities CAPABILITY_NAMED_IAM \
  --region us-east-1

Deploy to Vercel

bash
# Deploy to production
vercel --prod

# Or link to GitHub for automatic deployments
vercel git connect

Production Best Practices

1. Environment Stages

SST (automatic):

  • dev - Development stage (isolated resources)
  • staging - Staging stage
  • production - Production stage

Vercel (manual):

  • Development (preview deployments)
  • Production (main branch)

2. Secrets Management

SST:

bash
npx sst secrets set SECRET_NAME "value" --stage production

Vercel:

bash
vercel env add SECRET_NAME production

Never commit secrets to git.

3. Database Backups

Enable Point-in-Time Recovery (CloudFormation already includes this):

yaml
PointInTimeRecoverySpecification:
  PointInTimeRecoveryEnabled: true

Enable backups in SST:

typescript
const table = new Table(stack, 'AppTable', {
  // ...
  cdk: {
    table: {
      pointInTimeRecovery: true,
    },
  },
});

4. Monitoring

CloudWatch Alarms:

typescript
// stacks/Monitoring.ts
import { Alarm, Metric } from 'aws-cdk-lib/aws-cloudwatch';
import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { Topic } from 'aws-cdk-lib/aws-sns';

export function Monitoring({ stack }: StackContext) {
  const topic = new Topic(stack, 'AlertTopic');

  // DynamoDB throttle alarm
  const throttleAlarm = new Alarm(stack, 'DynamoDBThrottle', {
    metric: new Metric({
      namespace: 'AWS/DynamoDB',
      metricName: 'UserErrors',
      dimensionsMap: {
        TableName: table.tableName,
      },
      statistic: 'Sum',
    }),
    threshold: 10,
    evaluationPeriods: 1,
  });

  throttleAlarm.addAlarmAction(new SnsAction(topic));
}

Vercel Analytics:

  • Built-in web vitals
  • Function logs
  • Edge network metrics

5. Performance

Enable caching:

typescript
// next.config.js
module.exports = {
  headers: async () => [
    {
      source: '/api/:path*',
      headers: [
        {
          key: 'Cache-Control',
          value: 'public, s-maxage=60, stale-while-revalidate=120',
        },
      ],
    },
  ],
};

DynamoDB DAX (for read-heavy workloads):

typescript
// stacks/Database.ts
import { CfnCluster } from 'aws-cdk-lib/aws-dax';

const daxCluster = new CfnCluster(stack, 'DAXCluster', {
  clusterName: 'my-app-dax',
  nodeType: 'dax.t3.small',
  replicationFactor: 3,
  iamRoleArn: role.roleArn,
  subnetGroupName: subnetGroup.ref,
  securityGroupIds: [securityGroup.securityGroupId],
});

6. Security

DynamoDB encryption at rest (enabled by default):

typescript
const table = new Table(stack, 'AppTable', {
  // ...
  cdk: {
    table: {
      encryption: TableEncryption.AWS_MANAGED,
    },
  },
});

VPC for sensitive workloads:

typescript
import { Vpc } from 'aws-cdk-lib/aws-ec2';

const vpc = new Vpc(stack, 'VPC', {
  maxAzs: 2,
});

const site = new NextjsSite(stack, 'Site', {
  // ...
  cdk: {
    server: {
      vpc,
    },
  },
});

7. Cost Optimization

Use on-demand billing (default):

  • Pay only for what you use
  • No capacity planning
  • Good for variable workloads

Switch to provisioned for predictable workloads:

typescript
const table = new Table(stack, 'AppTable', {
  // ...
  cdk: {
    table: {
      billingMode: BillingMode.PROVISIONED,
      readCapacity: 5,
      writeCapacity: 5,
    },
  },
});

Enable auto-scaling:

typescript
const readScaling = table.cdk.table.autoScaleReadCapacity({
  minCapacity: 5,
  maxCapacity: 100,
});

readScaling.scaleOnUtilization({
  targetUtilizationPercent: 70,
});

CI/CD Pipeline

GitHub Actions (SST)

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

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

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Deploy to AWS
        run: npm run deploy
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
          GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
          GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}

GitHub Actions (Vercel)

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

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

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Rollback Strategy

SST:

bash
# List deployments
aws cloudformation list-stacks --region us-east-1

# Rollback to previous version
aws cloudformation cancel-update-stack --stack-name my-app-production

Vercel:

bash
# List deployments
vercel ls

# Promote previous deployment
vercel promote <deployment-url>

Health Checks

typescript
// app/api/health/route.ts
import { NextResponse } from 'next/server';
import { docClient, TABLE_NAME } from '@/lib/db/client';
import { DescribeTableCommand } from '@aws-sdk/client-dynamodb';

export async function GET() {
  try {
    // Check DynamoDB connection
    await docClient.send(new DescribeTableCommand({
      TableName: TABLE_NAME,
    }));

    return NextResponse.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json(
      {
        status: 'unhealthy',
        error: error instanceof Error ? error.message : 'Unknown error',
      },
      { status: 503 }
    );
  }
}

You deploy production-ready, monitored, secure applications to AWS. Period.

Didn't find tool you were looking for?

Be as detailed as possible for better results