While working on DynamoDB-Toolshack, I encountered a common challenge: ensuring fast and reliable performance for users worldwide. In todayβs digital landscape, users expect snappy responses no matter their location. Meeting this demand often requires deploying globally distributed infrastructure.
For those unfamiliar, DynamoDB-Toolshack is the ultimate DynamoDB Studio for rapidly evolving applications. It elevates your DynamoDB experience with a schema-aware admin interface, data monitors and migration tools.
This guide walks you through building and deploying a multi-region serverless API using Amazon API Gateway, AWS Lambda, DynamoDB Global Tables, and the AWS CDK (Cloud Development Kit).
π§± Architecture Overview
Weβll build the following architecture:
- Amazon API Gateway β Public HTTP API to handle client requests.
- AWS Lambda β Stateless compute service to process those requests.
- Amazon DynamoDB Global Tables β A fully replicated database for low-latency data access across multiple regions.
- AWS CDK β Infrastructure as code to define and deploy all resources.
Prerequisites
Before starting, make sure you:
- Have a Route 53 hosted zone for a top-level domain (e.g.,
example.com
), and plan to use a subdomain likeapi.example.com
. - Write code in TypeScript.
- Are familiar with the AWS CDK.
π¦ Step 1: Set Up Your CDK Project
Start by initializing a new CDK app:
# Create the folder
mkdir global-api-cdk && cd global-api-cdk
# Init the CDK App
npx cdk init app --language typescript
# Add some dependencies
npm add @types/aws-lambda @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
π Step 2: Choose Deployment Regions
Pick one primary region for global resources (e.g., us-east-1
) and one or more additional regions for API and data replication.
// lib/regions.ts
export const globalResourcesRegion = 'us-east-1';
export const replicatedRegions = ['eu-west-1', 'sa-east-1'];
export const allRegions = [globalResourcesRegion, ...replicatedRegions];
Each replicated region will host a replica of the API and DynamoDB table. The Global Table ensures data sync across all regions.
ποΈ Step 3: Define a DynamoDB Global Table
Create a GlobalResourcesStack
containing unique resources. This includes a TableV2
with replicationRegions
specified:
// lib/global.ts
import { Construct } from 'constructs';
import { Stack, StackProps } from 'aws-cdk-lib';
import { AttributeType, TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { replicatedRegions } from './regions';
export class GlobalResourcesStack extends Stack {
table: TableV2;
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
this.table = new TableV2(this, 'Table', {
tableName: 'global-table',
partitionKey: { name: 'id', type: AttributeType.STRING },
replicas: replicatedRegions.map(region => ({ region })),
});
}
}
Create regional stacks for replicated resources. You can import the TableV2
with the fromTableName
method:
// lib/local.ts
import { Construct } from 'constructs';
import { Stack, StackProps } from 'aws-cdk-lib';
import { ITableV2, TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { GlobalResourcesStack } from './global';
export class LocalResourcesStack extends Stack {
// π Use ITableV2 type as the table is in the global stack
table: ITableV2;
constructor(
scope: Construct,
id: string,
{
globalResources,
...props
}: { globalResources: GlobalResourcesStack } & StackProps,
) {
super(scope, id, props);
// π Use `fromTableName` to replace the stack region
this.table = TableV2.fromTableName(
this,
'Table',
globalResources.table.tableName,
);
}
}
Finally, let's define the CDK App like this:
// bin/global-api-cdk.ts
import { App } from 'aws-cdk-lib';
import { allRegions, globalResourcesRegion } from '../lib/regions';
import { GlobalResourcesStack } from '../lib/global';
import { LocalResourcesStack } from '../lib/local';
const app = new App();
const globalResources = new GlobalResourcesStack(app, 'GlobalResources', {
env: { region: globalResourcesRegion },
// π Important to pass References from a stack to another
crossRegionReferences: true,
});
for (const region of allRegions) {
const localResources = new LocalResourcesStack(
app,
`LocalResources${region}`,
{
env: { region },
globalResources,
crossRegionReferences: region !== globalResourcesRegion,
},
);
localResources.addDependency(globalResources);
}
π Step 4: Set Up Custom Domains
Let's import the top domain Hosted Zone (example.com
) and create a Hosted Zone for the API subdomain (api.example.com
). Hosted zones are global, so let's add it to the GlobalResourcesStack
:
import {
IHostedZone,
HostedZone,
RecordSet,
RecordTarget,
RecordType,
} from 'aws-cdk-lib/aws-route53';
class GlobalResourcesStack extends Stack {
...
topDomainHostedZone: IHostedZone;
apiSubDomainHostedZone: HostedZone;
apiSubDomainDelegationRecords: RecordSet;
constructor(...) {
...
this.topDomainHostedZone = HostedZone.fromHostedZoneAttributes(
this,
'TopDomainHostedZone',
// π Imported from your Top Domain configuration
{ zoneName: 'example.com', hostedZoneId: 'Z31867814UKQT2MAKXUTL' },
);
this.apiSubDomainHostedZone = new HostedZone(
this,
'ApiSubDomainHostedZone',
{ zoneName: 'api.example.com' },
);
// π (optional) To automatically append the delegation records
this.apiSubDomainDelegationRecords = new RecordSet(
this,
'ApiSubDomainDelegationRecords',
{
zone: this.topDomainHostedZone,
recordType: RecordType.NS,
recordName: this.apiSubDomainHostedZone.zoneName,
target: RecordTarget.fromValues(
...this.apiSubDomainHostedZone.hostedZoneNameServers!,
),
},
);
}
}
Next, let's create a Rest API and a regional Certificate for each replicated region:
import { IHostedZone } from 'aws-cdk-lib/aws-route53';
import {
Certificate,
CertificateValidation,
} from 'aws-cdk-lib/aws-certificatemanager';
import { RestApi } from 'aws-cdk-lib/aws-apigateway';
class LocalResourcesStack extends Stack {
...
// π Use IHostedZone type as the HostedZone is in the global stack
apiSubDomainHostedZone: IHostedZone;
apiCertificate: Certificate;
api: RestApi;
constructor(...) {
...
this.apiSubDomainHostedZone = globalResources.apiSubDomainHostedZone;
this.apiCertificate = new Certificate(this, 'ApiCertificate', {
domainName: this.apiSubDomainHostedZone.zoneName,
validation: CertificateValidation.fromDns(this.apiSubDomainHostedZone),
});
this.api = new RestApi(this, 'Api', {
domainName: {
domainName: this.apiSubDomainHostedZone.zoneName,
certificate: this.apiCertificate,
},
});
}
}
π°οΈ Step 5: Add Latency-Based Routing
The CDK doesnβt natively support latency records, but we can use this workaround:
// lib/latencyRecord.ts
import { ARecordProps, CfnRecordSet, ARecord } from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
export class LatencyRecord extends ARecord {
constructor(
scope: Construct,
id: string,
{ region, ...props }: { region: string } & ARecordProps,
) {
super(scope, id, props);
const recordSet = this.node.defaultChild as CfnRecordSet;
recordSet.region = region;
recordSet.setIdentifier = region;
}
}
And use it like so:
import { ApiGateway } from 'aws-cdk-lib/aws-route53-targets';
import { RecordTarget } from 'aws-cdk-lib/aws-route53';
import { LatencyRecord } from './latencyRecord';
class LocalResourcesStack extends Stack {
...
constructor(...) {
...
new LatencyRecord(this, 'ApiSubDomainLatencyRecord', {
zone: this.apiSubDomainHostedZone,
target: RecordTarget.fromAlias(new ApiGateway(this.api)),
region: this.region,
});
}
}
βοΈ Step 6: Add a Lambda Function
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime, Architecture } from 'aws-cdk-lib/aws-lambda';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
class LocalResourcesStack extends Stack {
...
lambda: NodejsFunction;
constructor(...) {
...
this.lambda = new NodejsFunction(this, 'Function', {
entry: 'lib/handler.ts',
handler: 'main',
environment: {
TABLE_NAME: this.table.tableName,
},
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
awsSdkConnectionReuse: true,
logGroup: new LogGroup(this, 'FunctionLogGroup', {
logGroupName: 'log-group',
retention: RetentionDays.THREE_MONTHS,
}),
bundling: {
minify: true,
keepNames: true,
sourceMap: true,
mainFields: ['module', 'main'],
externalModules: ['@aws-sdk/*'],
},
});
// π Add permissions to read data from the table
this.table.grantReadData(this.lambda);
// π Set the API as Lambda trigger
this.api.root
.resourceForPath('/object/{id}')
.addMethod('GET', new LambdaIntegration(this.lambda));
}
}
Hereβs a simple code file for our handler:
// lib/handler.ts
import type {
APIGatewayProxyEventBase,
APIGatewayProxyResult,
} from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const dynamoDbClient = new DynamoDBClient();
const documentClient = DynamoDBDocumentClient.from(dynamoDbClient);
export const main = async (
event: APIGatewayProxyEventBase<unknown>,
): Promise<APIGatewayProxyResult> => {
const { pathParameters } = event;
const id = pathParameters!.id;
const { Item } = await documentClient.send(
new GetCommand({ TableName: process.env.TABLE_NAME, Key: { id } }),
);
if (Item === undefined) {
return { statusCode: 404, body: JSON.stringify({ message: 'Not found' }) };
}
return { statusCode: 200, body: JSON.stringify(Item) };
};
π Step 7: Deploy and Test
cdk bootstrap
cdk deploy --all
Once deployed, test your endpoint at https://api.example.com/object/{id}
. Depending on your position, you should observe CloudWatch Logs in one region or another!
π Final Thoughts
Adding a Cognito UserPool
for authentication is easy, but beyond the scope of this guide.
This architecture has many benefits:
- Global low-latency access to data.
- Fully managed infrastructure with high availability.
- Pay-per-use with minimal operational overhead.
- Easily extendable to more regions or endpoints.
However, beware of:
- Eventual consistency: DynamoDB Global Tables introduce replication delays between regions.
- API Gateway limitations: Some features like API keys are region-specific and not globally synchronized.
π οΈ Conclusion
Deploying a global serverless API is simpler than ever with AWSβs managed services and the CDK. With just a few lines of code, you can serve users across the world with fast response times and consistent data. Whether you're building for mobile, IoT, or global web apps, this setup gives you performance, reliability, and scale.
Top comments (2)
Hi, amazing post!, I have a question:
I'm not yet familiar with AWS CDK and complex AWS integrations like this, on a quick thought, do you think it's possible to achieve the same outcome with even better latency and pricing by using Cloudfront to expose the Lambda instead of using API Gateway?
And also for Lambda streaming feature, which is only possible via Cloudfront.
Hi @namesmt !
The CDK is mostly a SDK in front of Cloudformation so if it's doable with Cloudformation it should be doable with the CDK.
As to using Cloudfront... You'll have to try, I'm not sure π One problem I see is that Cloudfront is a global service. One thing I did not mention is that latency based routing only works because the APIs are regional.
The other pattern that I've seen is to use a Cloudfront functions for dynamic routing. You can learn more in this article: medium.com/@d.kumarkaran20/dynamic...