Full-Stack Deployment with AWS Amplify Gen 2 and CDK

Moving from Amplify Gen 1 to Gen 2 — TypeScript-first backends, CDK under the hood, and per-developer sandboxes that actually work.

#aws#amplify#fullstack#deployment
Cover image for the article: Full-Stack Deployment with AWS Amplify Gen 2 and CDK

Why Amplify Gen 2 Changes the Equation

Amplify Gen 1 had a reputation problem. The CLI-driven workflow generated CloudFormation templates that were opaque, hard to customize, and nearly impossible to debug when deployments failed. Teams would start with Amplify for speed, then rip it out at the exact moment they needed it most — when complexity grew beyond the happy path.

Amplify Gen 2 is a fundamentally different product wearing the same brand. The backend is defined in TypeScript, CDK constructs are first-class citizens, and the deployment pipeline is Git-based with per-developer sandbox environments. It's what Amplify should have been from the start.

Amplify Gen 2 Architecture

Architecture: TypeScript-First Backend Definition

The Gen 2 backend lives in an amplify/ directory at your project root. Instead of CLI-generated JSON, you write TypeScript that compiles to CDK constructs:

// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { storage } from './storage/resource';
import { processOrder } from './functions/process-order/resource';

const backend = defineBackend({
  auth,
  data,
  storage,
  processOrder,
});

// Access underlying CDK constructs for customization
const dataStack = backend.data.stack;
const table = backend.data.resources.tables['Order'];

// Add a GSI that Amplify's schema DSL doesn't natively support
table.addGlobalSecondaryIndex({
  indexName: 'byCustomerDate',
  partitionKey: { name: 'customerId', type: AttributeType.STRING },
  sortKey: { name: 'createdAt', type: AttributeType.STRING },
  projectionType: ProjectionType.ALL,
});

The critical difference from Gen 1: you can always escape to raw CDK. Amplify provides high-level abstractions for common patterns (auth, data, storage), but when you need a custom GSI, a specific Lambda configuration, or an integration that Amplify doesn't model — you reach into the CDK construct tree directly.

Data Modeling: The Schema DSL

Amplify Gen 2's data layer uses a TypeScript DSL that generates DynamoDB tables, AppSync resolvers, and client-side types in one pass:

// amplify/data/resource.ts
import { defineData, a, type ClientSchema } from '@aws-amplify/backend';

const schema = a.schema({
  Order: a.model({
    customerId: a.string().required(),
    items: a.json().required(),
    total: a.float().required(),
    status: a.enum(['PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED']),
    createdAt: a.datetime(),
    shippedAt: a.datetime(),
  })
  .secondaryIndexes((index) => [
    index('customerId').sortKeys(['createdAt']).name('byCustomerDate'),
  ])
  .authorization((allow) => [
    allow.owner(),
    allow.group('admin').to(['read', 'update', 'delete']),
    allow.authenticated().to(['read']),
  ]),

  Customer: a.model({
    email: a.email().required(),
    name: a.string().required(),
    tier: a.enum(['FREE', 'PRO', 'ENTERPRISE']),
    orders: a.hasMany('Order', 'customerId'),
  })
  .authorization((allow) => [
    allow.owner(),
    allow.group('admin'),
  ]),
});

export type Schema = ClientSchema<typeof schema>;
export const data = defineData({ schema });

The authorization rules compile to AppSync resolver logic — no Lambda authorizers needed for standard CRUD patterns. The type export (Schema) generates client-side types that flow through to your frontend components.

Per-Developer Sandbox Environments

The feature that sold our team: every developer gets an isolated AWS environment for their branch. No more "don't deploy to staging, I'm testing something":

# Start a personal sandbox (creates isolated resources)
npx ampx sandbox

# Sandbox watches for file changes and hot-deploys
# Each developer gets their own:
# - Cognito User Pool
# - DynamoDB tables
# - AppSync API
# - S3 buckets
# - Lambda functions

Sandboxes use your personal AWS credentials and create resources with a developer-specific prefix. The sandbox process watches your amplify/ directory and hot-deploys changes in under 20 seconds for most modifications.

Benchmarks: Gen 1 vs Gen 2 Developer Experience

We migrated a mid-size application (12 models, 8 Lambda functions, Cognito auth, S3 storage) from Gen 1 to Gen 2:

MetricAmplify Gen 1Amplify Gen 2Improvement
Initial deployment time8-12 min3-5 min-60%
Incremental deploy (data model change)4-6 min15-45 sec-90%
Time to add custom CDK resourceHours (eject required)Minutes (inline)Dramatic
CI/CD pipeline complexityHigh (amplify.yml + hooks)Low (Git-based)Simplified
Type safety (backend → frontend)Partial (codegen)Full (schema DSL)Complete
Debugging failed deploysPainful (nested CFN)Clear (CDK errors)Significantly better

The incremental deploy improvement is the game-changer for developer velocity. Gen 1 would re-synthesize and diff the entire CloudFormation stack for any change. Gen 2's sandbox uses targeted hot-swap deployments that only update the changed resource.

Production Deployment: Git-Based Pipeline

For production, Amplify Gen 2 uses a Git-based deployment pipeline that's configured in the Amplify console or via CDK:

// amplify/backend.ts — production configuration
import { defineBackend } from '@aws-amplify/backend';
import * as cdk from 'aws-cdk-lib';

const backend = defineBackend({ auth, data, storage });

// Production-specific configuration
const authStack = backend.auth.stack;

// Add WAF to Cognito hosted UI
const waf = new cdk.aws_wafv2.CfnWebACL(authStack, 'AuthWaf', {
  scope: 'REGIONAL',
  defaultAction: { allow: {} },
  rules: [
    {
      name: 'RateLimit',
      priority: 1,
      action: { block: {} },
      statement: {
        rateBasedStatement: {
          limit: 100,
          aggregateKeyType: 'IP',
        },
      },
      visibilityConfig: {
        sampledRequestsEnabled: true,
        cloudWatchMetricsEnabled: true,
        metricName: 'AuthRateLimit',
      },
    },
  ],
  visibilityConfig: {
    sampledRequestsEnabled: true,
    cloudWatchMetricsEnabled: true,
    metricName: 'AuthWaf',
  },
});

Migration Strategy: Gen 1 to Gen 2

Our migration took 3 weeks for a team of 2. The approach:

  1. Week 1: Recreate the data schema in Gen 2's TypeScript DSL. Run Gen 1 and Gen 2 in parallel.
  2. Week 2: Migrate auth configuration, custom Lambda functions, and storage buckets. Verify parity with integration tests.
  3. Week 3: Cut over DNS, decommission Gen 1 resources, clean up.

The hardest part was migrating Cognito User Pools — Gen 2 creates new pools rather than importing existing ones. We used a Lambda migration trigger to transparently re-authenticate users on first login.

Operational Considerations

What Gen 2 handles well:

  • Rapid prototyping with immediate type safety
  • Multi-environment isolation (sandbox per developer)
  • Standard CRUD patterns with authorization
  • Frontend framework integration (React, Next.js, Vue)

Where you'll hit limits:

  • Complex event-driven architectures (beyond simple Lambda triggers)
  • Multi-region deployments (Amplify is single-region)
  • Advanced networking (VPC configurations are possible but not ergonomic)
  • Large teams with many microservices (Amplify is best for bounded applications)

Conclusion

Amplify Gen 2 is the right tool for full-stack applications where the backend is an enabler for the frontend, not the core product. If your value is in the user experience and your backend is standard patterns (auth, CRUD, storage, functions), Gen 2 eliminates weeks of CDK boilerplate while preserving escape hatches.

The TypeScript-first approach means your backend definition is type-checked, auto-completed, and version-controlled the same way as your application code. Per-developer sandboxes eliminate environment conflicts. And when you need something Amplify doesn't model — you just write CDK.

Don't use it for microservice architectures or complex event-driven systems. Do use it for products where shipping features matters more than infrastructure novelty.

Comments

    No comments yet. Be the first to share your thoughts.