MongoDB Atlas with AWS CDK and VPC Peering: A Cost-Effective Approach
When building cloud applications that require MongoDB Atlas, establishing secure and cost-effective connectivity to your existing AWS infrastructure is crucial. This comprehensive tutorial demonstrates how to create an AWS CDK stack that deploys a MongoDB Atlas cluster with VPC peering to connect to your existing VPC network - a solution that significantly reduces costs compared to using MongoDB’s VPC endpoints.
Why VPC Peering Over VPC Endpoints?
Before diving into the implementation, let’s understand the cost benefits:
- VPC Endpoints: MongoDB VPC endpoints can cost $45-90+ per month depending on usage
- VPC Peering: Typically costs $0.01 per GB of data transferred across peering connections
- Network Performance: Direct peering often provides better latency and throughput
- Simplified Architecture: No additional endpoint management overhead
For most applications, VPC peering provides a more economical solution while maintaining enterprise-grade security.
Prerequisites
Before we begin, ensure you have:
- MongoDB Atlas Account: Sign up via AWS Marketplace for integrated billing
- MongoDB Atlas Programmatic API Key (PAK): Create an API key with Organization Project Creator permissions
- AWS Account: Configured with AWS CLI and appropriate permissions
- Store MongoDB Atlas PAK in AWS Secrets Manager: For secure credential management
- Node.js 18+: Required for AWS CDK
- Activate CloudFormation Resources: Enable these third-party resources in your target AWS region via the CloudFormation Registry:
MongoDB::Atlas::Project
MongoDB::Atlas::Cluster
MongoDB::Atlas::DatabaseUser
MongoDB::Atlas::ProjectIpAccessList
MongoDB::Atlas::NetworkContainer
MongoDB::Atlas::NetworkPeering
Important: You must activate these MongoDB Atlas CloudFormation resources in each AWS region where you plan to deploy. This is a one-time setup per region.
Step 1: Initial Setup
Install AWS CDK
npm install -g aws-cdk
Bootstrap Your AWS Environment
cdk bootstrap aws://ACCOUNT_NUMBER/REGION
Replace ACCOUNT_NUMBER
with your AWS account number and REGION
with your target region.
Initialize a New CDK Project
mkdir mongodb-atlas-vpc-peering
cd mongodb-atlas-vpc-peering
cdk init app --language typescript
Install Required Dependencies
npm install awscdk-resources-mongodbatlas
npm install @types/node
Step 2: Understanding the Architecture
Our CDK construct will create the following infrastructure:
┌─────────────────────┐ VPC Peering ┌─────────────────────┐
│ AWS VPC │◄─────────────────►│ MongoDB Atlas │
│ │ │ │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ Private Subnet │ │ │ │ Cluster │ │
│ │ │ │ │ │ (M10+) │ │
│ │ ┌─────────────┐ │ │ │ │ │ │
│ │ │Lambda Function│ │ │ │ └─────────────────┘ │
│ │ └─────────────┘ │ │ │ │
│ └─────────────────┘ │ └─────────────────────┘
│ │
└─────────────────────┘
Components created:
- MongoDB Atlas Project: Container for your cluster
- Network Container: Atlas-side networking configuration
- VPC Peering Connection: Secure tunnel between Atlas and AWS VPC
- MongoDB Cluster: The actual database with appropriate specs
- Database User: Authentication credentials stored in AWS Secrets Manager
- IP Access List: Security rules for private network access
- Route Table Updates: Automatic routing configuration for your VPC subnets
Step 3: Create the MongoDB Atlas Infrastructure Construct
Create a new file lib/mongodb-atlas-construct.ts
and implement the following construct:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as atlas from 'awscdk-resources-mongodbatlas';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { SetRequired } from 'type-fest';
interface MongoAtlasConstructProps {
ebsVolumeType: string,
instanceSize: string,
nodeCount: number,
region: string,
vpc: ec2.IVpc
enableBackup: boolean
atlasCidr: string
autoScaling?: atlas.AdvancedAutoScaling
dbName: string
dbUserName: string
projectName: string
atlasOrgId: string
atlasProfileName: string
accessList?: atlas.IpAccessListProps['accessList']
}
export class MongoAtlasConstruct extends Construct {
mCluster: atlas.CfnCluster
secret: secretsmanager.Secret
atlasRegion: string
atlasNetworkContainer: atlas.CfnNetworkContainer
atlasNetworkPeering: atlas.CfnNetworkPeering
mProject: atlas.CfnProject
clusterName: string
constructor(scope: Construct, id: string, private readonly props: MongoAtlasConstructProps) {
super(scope, id);
// Validate instance size - only dedicated instances are supported
if (["M0", "M2", "M5"].includes(props.instanceSize)) {
throw new Error(`Instance size ${props.instanceSize} is not supported. Only dedicated instances (M10 and above) are allowed.`);
}
// Since we only support dedicated instances, use private access via VPC CIDR
// and concatenate with any additional access list from props
let accessList: atlas.IpAccessListProps['accessList'] = [
{ cidrBlock: props.vpc.vpcCidrBlock, comment: 'Private access via VPC CIDR' },
]
if (props.accessList) {
accessList = accessList.concat(props.accessList)
}
this.atlasRegion = props.region.toUpperCase().replace(/-/g, "_")
// Use AWS provider for dedicated instances
let providerName = atlas.AdvancedRegionConfigProviderName.AWS
this.secret = new secretsmanager.Secret(this, 'DatabaseSecret', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: props.dbUserName }),
generateStringKey: 'password',
passwordLength: 12,
excludePunctuation: true,
},
});
// Configure backup options for dedicated instances
let backupOpts = {
pitEnabled: props.enableBackup,
backupEnabled: props.enableBackup,
terminationProtectionEnabled: props.enableBackup,
}
this.atlasVpcPeering(id,
{
dbUserProps: {
username: this.secret.secretValueFromJson('username').unsafeUnwrap(),
password: this.secret.secretValueFromJson('password').unsafeUnwrap()
},
clusterProps: {
name: 'Cluster',
...backupOpts,
replicationSpecs: [
{
numShards: 1,
advancedRegionConfigs: [
{
electableSpecs: {
ebsVolumeType: props.ebsVolumeType,
instanceSize: props.instanceSize,
nodeCount: props.nodeCount
},
priority: 7,
regionName: this.atlasRegion,
providerName,
backingProviderName: "AWS",
autoScaling: props.autoScaling
}]
}],
},
projectProps: {
orgId: props.atlasOrgId,
name: props.projectName,
},
ipAccessListProps: {
accessList: accessList,
},
profile: props.atlasProfileName,
}
)
}
atlasVpcPeering(id: string, props: atlas.AtlasBasicProps & {
dbUserProps: SetRequired<atlas.DatabaseUserProps, 'username' | 'password'>,
}) {
this.mProject = new atlas.CfnProject(this, "Project", {
profile: props.profile,
name: props.projectProps.name ||
projectDefaults.projectName.concat(String(randomNumber())),
...props.projectProps,
});
// Create network container and peering for dedicated instances
this.atlasNetworkContainer = new atlas.CfnNetworkContainer(this, 'NetworkContainer', {
vpcId: this.props.vpc.vpcId,
atlasCidrBlock: this.props.atlasCidr,
projectId: this.mProject.attrId,
regionName: this.atlasRegion,
profile: props.profile,
})
this.atlasNetworkPeering = new atlas.CfnNetworkPeering(this, 'NetworkPeering', {
containerId: this.atlasNetworkContainer.attrId,
projectId: this.mProject.attrId,
vpcId: this.props.vpc.vpcId,
accepterRegionName: this.props.vpc.env.region,
awsAccountId: this.props.vpc.env.account,
profile: props.profile,
routeTableCidrBlock: this.props.vpc.vpcCidrBlock,
});
// Create a new MongoDB Atlas Cluster and pass project ID
this.clusterName = props.clusterProps.name ||
clusterDefaults.clusterName.concat(String(randomNumber()))
this.mCluster = new atlas.CfnCluster(this, "Cluster", {
profile: props.profile,
name: this.clusterName,
projectId: this.mProject.attrId,
clusterType: clusterDefaults.clusterType,
...props.clusterProps,
});
// Add dependencies for network resources
this.mCluster.addDependency(this.atlasNetworkContainer)
this.mCluster.addDependency(this.atlasNetworkPeering)
// Create a new MongoDB Atlas Database User
const _mDBUser = new atlas.CfnDatabaseUser(this, "User", {
profile: props.profile,
...props.dbUserProps,
databaseName: props.dbUserProps?.databaseName || dbDefaults.dbName,
projectId: this.mProject.attrId,
roles: props.dbUserProps?.roles || dbDefaults.roles,
});
// Create a new MongoDB Atlas Project IP Access List
const _ipAccessList = new atlas.CfnProjectIpAccessList(this, "IpAccess", {
profile: props.profile,
projectId: this.mProject.attrId,
...props.ipAccessListProps,
});
// Create routes from private subnets to Atlas, allow your application to access the cluster
const selectSubnets = this.props.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnets
selectSubnets.forEach((subnet, index) => {
new ec2.CfnRoute(this, 'AwsPeerToAtlasRoute' + index, {
routeTableId: subnet.routeTable.routeTableId,
destinationCidrBlock: this.props.atlasCidr,
vpcPeeringConnectionId: this.atlasNetworkPeering.attrConnectionId,
});
})
}
getConnectionHostname() {
const uri = this.mCluster.getAtt("ConnectionStrings.StandardSrv")?.toString() ?? ''
const domain = cdk.Fn.select(2, cdk.Fn.split("/", uri))
return domain
}
getDefaultDBName() {
return this.props.dbName
}
getUsername() {
return ecs.Secret.fromSecretsManager(this.secret, 'username')
}
getPassword() {
return ecs.Secret.fromSecretsManager(this.secret, 'password')
}
}
/** @type {*} */
const projectDefaults = {
projectName: "atlas-project-",
};
/** @type {*} */
const dbDefaults = {
dbName: "admin",
roles: [
{
roleName: "atlasAdmin",
databaseName: "admin",
},
],
};
/** @type {*} */
const clusterDefaults = {
clusterName: "atlas-cluster-",
clusterType: "REPLICASET",
};
/**
* @description
* @export
* @class AtlasBasic
* @extends {Construct}
*/
function randomNumber() {
const min = 10;
const max = 9999999;
return Math.floor(Math.random() * (max - min + 1) + min);
}
Step 4: Create the Main Stack
Update your main stack file (e.g., lib/mongodb-atlas-vpc-peering-stack.ts
):
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { MongoAtlasConstruct } from './mongodb-atlas-construct';
import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class MongodbAtlasVpcPeeringStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Get your VPC ready, or use the existing one
const vpc = new Vpc(this, "Vpc", {
maxAzs: 2,
natGateways: 1, // 1 Nat Gateway, testing only
subnetConfiguration: [
{
cidrMask: 24,
name: "Public",
subnetType: SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: "Egress",
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 24,
name: "Private",
subnetType: SubnetType.PRIVATE_ISOLATED,
},
],
});
// Define your MongoDB Atlas configuration
// Replace them your configurations, or use context, environment variables as whatever you prefer
const atlasOrgId = this.node.tryGetContext('mongodb-atlas:org-id'); // Replace with your Atlas Org ID
const atlasProfileName = this.node.tryGetContext('mongodb-atlas:profile'); // Replace with your Atlas profile name
const dbName = 'my-app'; // Replace with your desired database name
const dbUserName = 'my-app-user'; // Replace with your desired database user name
const projectName = 'my-app-project'; // Replace with your desired project name
const atlasCidr = '192.168.8.0/21'; // Replace with your desired Atlas CIDR block
const region = props?.env?.region || 'ap-southeast-2'; // Provide the stack region, default to 'ap-southeast-2'
const mongoAtlasConstruct = new MongoAtlasConstruct(this, 'MongoDBCluster', {
atlasOrgId: atlasOrgId,
atlasProfileName: atlasProfileName,
projectName: projectName,
dbName: dbName,
dbUserName: dbUserName,
ebsVolumeType: 'STANDARD',
instanceSize: 'M10', // Use M10+ for VPC peering support
nodeCount: 3,
region: region,
vpc: vpc,
enableBackup: false, // Enable backups and termination protection
atlasCidr: atlasCidr,
});
// Create a Lambda function to test MongoDB connectivity, inside Egress Subnet
const connectivityTestLambda = new NodejsFunction(this, 'MongoDBConnectivityTest', {
runtime: Runtime.NODEJS_20_X,
handler: 'index.handler',
entry: 'lambda/connectivity-test.ts', // Path to your Lambda function code
vpc: vpc,
vpcSubnets: {
subnetType: SubnetType.PRIVATE_WITH_EGRESS
},
environment: {
MONGODB_HOST_NAME: mongoAtlasConstruct.getConnectionHostname(),
MONGODB_DB_NAME: mongoAtlasConstruct.getDefaultDBName(),
MONGODB_SECRET_ARN: mongoAtlasConstruct.secret.secretArn, // Use the secret ARN for credentials
},
timeout: cdk.Duration.seconds(30),
memorySize: 256,
description: 'Lambda function to test MongoDB Atlas connectivity through VPC peering'
});
mongoAtlasConstruct.secret.grantRead(connectivityTestLambda);
// Output the Lambda function details for easy testing
new cdk.CfnOutput(this, 'ConnectivityTestLambdaArn', {
value: connectivityTestLambda.functionArn,
description: 'ARN of the MongoDB connectivity test Lambda function'
});
new cdk.CfnOutput(this, 'ConnectivityTestLambdaName', {
value: connectivityTestLambda.functionName,
description: 'Name of the MongoDB connectivity test Lambda function'
});
}
}
Step 5: Deploy Your Infrastructure
Set CDK Context
Create a cdk.context.json
file with your MongoDB Atlas configuration:
{
"mongodb-atlas:org-id": "YOUR_MONGODB_ATLAS_ORG_ID",
"mongodb-atlas:profile": "default"
}
Preview Changes
cdk diff
Deploy the Stack
cdk deploy
Step 6: Accept and Verify VPC Peering Connection
After deployment, go to your VPC peering and accept the connection.
- MongoDB Atlas Console: Check that the peering connection shows as “Available”
- AWS VPC Console: Verify the peering connection is active
- Route Tables: Confirm routes to Atlas CIDR are properly configured
- Connectivity Test: Test database connection from an EC2 instance in your VPC
Step 7: Test the MongoDB connection
First, create the Lambda function dependencies. Add to your package.json
:
{
"dependencies": {
"mongodb": "^6.0.0",
"@aws-sdk/client-secrets-manager": "^3.0.0"
}
}
Then create the Lambda function at lambda/connectivity-test.ts
:
const { MongoClient } = require('mongodb');
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
exports.handler = async () => {
const hostName = process.env.MONGODB_HOST_NAME;
const dbName = process.env.MONGODB_DB_NAME;
const secretArn = process.env.MONGODB_SECRET_ARN;
if (!hostName || !dbName || !secretArn) {
return {
statusCode: 400,
body: JSON.stringify({
success: false,
message: 'MongoDB configuration not provided (missing MONGODB_HOST_NAME, MONGODB_DB_NAME, or MONGODB_SECRET_ARN)'
})
};
}
let client;
try {
console.log('Retrieving MongoDB credentials from Secrets Manager...');
// Get credentials from Secrets Manager
const secretsClient = new SecretsManagerClient({ region: process.env.AWS_REGION });
const secretCommand = new GetSecretValueCommand({ SecretId: secretArn });
const secretResponse = await secretsClient.send(secretCommand);
if (!secretResponse.SecretString) {
throw new Error('Secret value is empty');
}
const secretData = JSON.parse(secretResponse.SecretString);
const username = secretData.username;
const password = secretData.password;
if (!username || !password) {
throw new Error('Username or password not found in secret');
}
// Construct the MongoDB connection string
const connectionString = `mongodb+srv://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${hostName}/${dbName}?retryWrites=true&w=majority`;
console.log('Attempting to connect to MongoDB Atlas...');
console.log('Hostname:', hostName);
console.log('Database:', dbName);
client = new MongoClient(connectionString);
await client.connect();
// Test the connection
const admin = client.db('admin');
const result = await admin.command({ ping: 1 });
// Additional connectivity tests
const testDb = client.db(dbName);
const collections = await testDb.listCollections().toArray();
console.log('Successfully connected to MongoDB Atlas');
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Successfully connected to MongoDB Atlas',
ping: result,
database: dbName,
collectionsCount: collections.length,
collections: collections.map((c: any) => c.name),
timestamp: new Date().toISOString(),
vpcInfo: {
availabilityZone: process.env.AWS_REGION,
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME
}
})
};
} catch (error: any) {
console.error('Failed to connect to MongoDB Atlas:', error);
return {
statusCode: 500,
body: JSON.stringify({
success: false,
message: 'Failed to connect to MongoDB Atlas',
error: error.message,
errorCode: error.code,
timestamp: new Date().toISOString(),
vpcInfo: {
availabilityZone: process.env.AWS_REGION,
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME
}
})
};
} finally {
if (client) {
await client.close();
}
}
};
Test with the function
Cost Optimization Tips
- Choose Appropriate Instance Sizes: Start with M10 and scale based on actual usage
- Enable Auto-scaling: Configure compute and storage auto-scaling for cost efficiency
- Monitor Data Transfer: VPC peering charges are based on data transferred
- Use Private Endpoints: Keep traffic within AWS network to minimize charges
- Backup Strategy: Balance backup retention with storage costs
Security Best Practices
- Restrict IP Access: Use VPC CIDR blocks instead of 0.0.0.0/0
- Rotate Credentials: Implement regular password rotation using AWS Secrets Manager
- Network Segmentation: Use private subnets for database connections
- Monitoring: Enable MongoDB Atlas and AWS CloudTrail logging
- Encryption: Ensure encryption in transit and at rest
Troubleshooting Common Issues
Peering Connection Stuck in “Pending”
- Verify AWS account ID and VPC ID are correct
- Check that CIDR blocks don’t overlap
- Ensure proper IAM permissions for Atlas
Cannot Connect to Database
- Verify security groups allow MongoDB traffic (port 27017)
- Check route table configurations
- Confirm IP access list includes VPC CIDR
High Data Transfer Costs
- Review application connection patterns
- Implement connection pooling
- Consider data locality and read preferences
Cleanup
To avoid unnecessary charges, clean up resources when no longer needed:
cdk destroy
Note: This will delete the MongoDB Atlas cluster and all data. Ensure you have proper backups before destroying production resources.
Conclusion
This approach provides a robust, cost-effective solution for connecting MongoDB Atlas to your AWS infrastructure. By using VPC peering instead of VPC endpoints, you can achieve:
- 60-80% cost reduction compared to VPC endpoints
- Better network performance with direct peering
- Enhanced security with private network connectivity
- Simplified management through Infrastructure as Code
The CDK construct we’ve built is reusable and can be easily adapted for different environments (development, staging, production) by adjusting the parameters.
For production deployments, consider implementing additional monitoring, alerting, and backup strategies to ensure high availability and data protection.
The full code can be accessed at https://github.com/phanluanint/mongodb-atlas-vpc-peering