Skip to content

Migrating Amplify Backend to Docker for Local Testing

0

Hello AWS Community,

Over the past few months, my team’s backend environment has heavily relied on AWS Amplify’s integration with GitHub branches. Our typical workflow involves pulling a branch, making local changes, pushing the updates, and waiting for Amplify to handle the deployment.

While this setup works, it’s proven to be inefficient — each build takes around 4–5 minutes, and occasionally the build fails altogether. I’ve tried researching alternatives for backend deployment and local testing, but haven’t found anything concrete or straightforward.

To address this, I’m exploring the idea of containerizing our backend using Docker, allowing us to test and verify Lambda functions locally with dummy data before committing to a full deployment. This would hopefully improve developer velocity and reduce build failures.

My questions:

  1. Are there recommended resources or best practices for migrating an Amplify backend to a Docker-based setup?
  2. Has anyone in the community faced a similar situation or successfully implemented a similar solution?
  3. Any advice on tools to emulate Amplify’s backend locally for testing (especially Lambda functions)?

Any insights or guidance would be greatly appreciated. Thanks in advance!

2 Answers
1
Accepted Answer

For your goal of improving local testing for Amplify backends, there are a few approaches you can consider:

For Amplify Gen 2, you can use the cloud sandbox feature which is designed specifically for faster local development iterations. You can run npx ampx sandbox in a terminal (concurrent with your frontend dev server) to deploy your cloud sandbox. This creates a development environment that allows for quicker testing without waiting for full deployments. The sandbox will "hotswap" resources as you make changes, which is much faster than full redeployments.

If you're looking to containerize for local testing, LocalStack might be a good solution. LocalStack is a Docker container that can emulate various AWS services locally including Lambda functions, S3 buckets, DynamoDB tables, CloudWatch, and Step Functions. This would allow you to test your backend infrastructure without deploying to AWS.

For container-based local testing, you'll need to:

  1. Decide whether container builds will run on developers' machines or remote machines
  2. Create a local deployment mechanism (using scripts or tools like Docker Compose)
  3. Deploy necessary infrastructure components like databases in a lightweight fashion

Unfortunately, deploying Docker containers directly on AWS Amplify isn't currently possible, as Amplify is primarily designed for hosting static websites and serverless web applications. If you want to continue using Docker containers for your production environment, you might need to consider alternative AWS services like AWS App Runner, Amazon ECS/EKS, or AWS Fargate.

For your specific use case of testing Lambda functions locally, the Amplify sandbox approach (for Gen 2) or LocalStack would be the most direct solutions to improve developer velocity and reduce build failures.
Sources
Fullstack TypeScript: Reintroducing AWS Amplify | Front-End Web & Mobile
Workload architecture - Container Build Lens
Test AWS infrastructure by using LocalStack and Terraform Tests - AWS Prescriptive Guidance
Deploying a Docker Container on AWS Amplify | AWS re:Post

answered 9 months ago
1

For those using Amplify Gen 2, the cloud sandbox feature provides an excellent solution designed specifically for faster local development iterations. By running npx ampx sandbox in a terminal alongside your frontend dev server, you can deploy a cloud sandbox environment. This creates a development setup that enables quick testing without the need for full deployments. One of the key advantages is the sandbox's "hotswap" capability for resources as changes are made, significantly reducing the time compared to complete re-deployments.

For developers interested in containerized local testing, LocalStack presents a compelling option. As a Docker container, LocalStack can emulate various AWS services locally, including Lambda functions, S3 buckets, DynamoDB tables, CloudWatch, and Step Functions. This enables comprehensive testing of backend infrastructure without the need for AWS deployments.

When implementing container-based local testing, several key decisions need to be made. Teams must determine whether container builds will run on developers' machines or remote machines, establish a local deployment mechanism using tools like scripts or Docker Compose, and implement lightweight versions of necessary infrastructure components such as databases.

It's important to note that AWS Amplify doesn't currently support direct Docker container deployment, as its primary focus is on hosting static websites and serverless web applications. If Docker containers are essential for your production environment, you might need to explore alternative AWS services such as AWS App Runner, Amazon ECS/EKS, or AWS Fargate.

For teams specifically focused on testing Lambda functions locally, two primary solutions stand out: the Amplify sandbox approach (for Gen 2) or LocalStack. Both options can effectively improve developer velocity and reduce build failures, though their implementation approaches differ significantly.

The Amplify Gen2 Solution

Amplify Gen2's sandbox environment emerges as a more elegant solution to these challenges. Instead of managing containers, developers can leverage native AWS service emulation, providing a built-in local development environment that offers instant feedback on changes. This approach eliminates the need for container management while maintaining a production-like testing environment.

Implementation Architecture

The project structure follows a clean, modular organization. At its core, the main backend configuration file (backend.ts) serves as the central point for resource definition. Function configurations handle specific runtime settings and permissions, while handlers contain the actual business logic. This separation of concerns promotes maintainability and scalability.

Workflow Transformation

The traditional GitHub-based workflow, with its lengthy cycle of pull-commit-push-wait, transforms into a more efficient process with Gen2. Developers can start a local sandbox environment, make changes with instant feedback, thoroughly test locally, and push only verified changes. This dramatically reduces the feedback loop and minimizes deployment failures.

Developer Productivity Benefits

The immediate impact on developer productivity is substantial. Changes reflect instantly in the local environment, eliminating the need to wait for deployments. Local testing capabilities ensure issues are caught early, while TypeScript integration provides additional safety through static typing. The combination of hot reloading and real AWS service emulation creates a robust development experience.

Amplify Gen2 Simple Architecture for Replication

## Directory Structure

```plaintext
amplify-gen2-project/
├── amplify/
│   ├── backend.ts                    # Main backend configuration
│   └── backend/
│       └── function/
│           ├── services/             # Business logic services
│           │   └── dataService.ts    # Service implementation
│           ├── types/               # Type definitions
│           │   └── common.ts        # Shared interfaces
│           ├── utils/              # Utility functions
│           │   └── response.ts     # Response handlers
│           ├── mainFunction.ts     # Function configuration
│           └── handler.ts          # Main request handler
├── package.json
└── tsconfig.json

Step-by-Step Files

  1. package.json
{
  "name": "amplify-gen2-project",
  "version": "1.0.0",
  "dependencies": {
    "@aws-amplify/backend": "latest",
    "@aws-amplify/backend-cli": "latest",
    "typescript": "^5.0.0"
  },
  "scripts": {
    "start": "npx ampx sandbox",
    "deploy": "npx ampx push"
  }
}
  1. tsconfig.json
{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "lib": ["es2018"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "rootDir": "."
  },
  "include": ["amplify/**/*"],
  "exclude": ["node_modules"]
}
  1. amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { mainFunction } from './backend/function/mainFunction';

const backend = defineBackend({
  mainFunction,
});

export default backend;
  1. amplify/backend/function/types/common.ts
export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  error?: string;
  metadata: {
    timestamp: string;
  };
}

export interface RequestContext {
  method: string;
  path: string;
  query: Record<string, string>;
  body: any;
}
  1. amplify/backend/function/utils/response.ts
import { ApiResponse } from '../types/common';

export const createResponse = (
  statusCode: number,
  response: ApiResponse,
  headers: Record<string, string> = {}
) => ({
  statusCode,
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
    ...headers
  },
  body: JSON.stringify(response)
});
  1. amplify/backend/function/services/dataService.ts
export class DataService {
  private static store: Record<string, any> = {};

  static async getData(id?: string) {
    if (id) {
      return this.store[id];
    }
    return Object.values(this.store);
  }

  static async saveData(data: any) {
    const id = `item_${Date.now()}`;
    this.store[id] = { id, ...data };
    return this.store[id];
  }
}
  1. amplify/backend/function/mainFunction.ts
import { defineFunction } from '@aws-amplify/backend';
import * as path from 'path';

export const mainFunction = defineFunction({
  name: 'mainFunction',
  entryPoint: path.join(__dirname, 'handler.ts'),
  runtime: 'nodejs20.x'
});
  1. amplify/backend/function/handler.ts
import { RequestContext } from './types/common';
import { DataService } from './services/dataService';
import { createResponse } from './utils/response';

export const handler = async (event: any) => {
  try {
    const context: RequestContext = {
      method: event.requestContext?.http?.method || 'UNKNOWN',
      path: event.rawPath || '/',
      query: event.queryStringParameters || {},
      body: event.body ? JSON.parse(event.body) : {}
    };

    let result;
    switch (context.method) {
      case 'GET':
        result = await DataService.getData(context.query.id);
        break;
      case 'POST':
        result = await DataService.saveData(context.body);
        break;
      default:
        return createResponse(405, {
          success: false,
          error: 'Method not allowed',
          metadata: { timestamp: new Date().toISOString() }
        });
    }

    return createResponse(200, {
      success: true,
      data: result,
      metadata: { timestamp: new Date().toISOString() }
    });

  } catch (error) {
    return createResponse(500, {
      success: false,
      error: 'Internal server error',
      metadata: { timestamp: new Date().toISOString() }
    });
  }
};

Usage

  1. Setup:
mkdir amplify-gen2-project
cd amplify-gen2-project
npm init -y
npm install @aws-amplify/backend @aws-amplify/backend-cli typescript
  1. Create the directory structure and files as shown above

  2. Configure AWS:

npx ampx configure profile
  1. Start Local Development:
npm start
  1. Test Endpoints:
# GET request
curl https://your-function-url/

# POST request
curl -X POST https://your-function-url/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Item"}'

This architecture provides:

  • Clean separation of concerns
  • Type safety
  • Easy extensibility
  • Consistent error handling
  • Simple local testing

Reference Links:

https://repost.aws/questions/QUX2wFDv-IQGmXgL33CQyXWg/deploying-a-docker-container-on-aws-amplify

https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/test-aws-infra-localstack-terraform.html

https://aws.amazon.com/blogs/mobile/amplify-gen2-ga/

AWS
answered 9 months ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.