Deploy a MongoDB Atlas cluster on GCP with CDKTF using TypeScript
Cloud DevOps @ smallfries.digital
Date: 2023-03-26
HashiCorp announced the general availability of Terraform CDK back in August 2022, and since then we've seen the arrival of several enhancements and new pre-built providers-many available through the Construct Hub. The beauty of CDKTF is that you can generate the code bindings for virtually any existing Terraform provider by simply using the cdktf cli and you can use the existing Terraform registry to look up a provider's resource and argument reference.
The source code for this series can be found here: https://github.com/smallfriesdigital/cdktf-mongodbatlas-gcp.
Let's dive head first into initializng a new cdktf project within our working dir that uses the GCP provider:
cdktf init --template=typescript --local --providers="google@~>4.0"
Next let's add the pre-built MongoDB Atlas provider:
cdktf provider add mongodb/mongodbatlas
OK, so now let's update MyStack in main.ts with the following CDKTF TypeScript code:
import { Construct } from "constructs";
import { App, TerraformStack, TerraformVariable } from "cdktf";
import { MongodbatlasProvider } from "@cdktf/provider-mongodbatlas/lib/provider";
import { Project } from "@cdktf/provider-mongodbatlas/lib/project";
import { Cluster } from "@cdktf/provider-mongodbatlas/lib/cluster";
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// define secure environment variables (won't end up in cdk.tf.json)
const publicKey = new TerraformVariable(this, "publicKey", {
type: "string",
description: "MongoDB Atlas Org Public Key",
sensitive: true,
});
const privateKey = new TerraformVariable(this, "privateKey", {
type: "string",
description: "MongoDB Atlas Org Private Key",
sensitive: true,
});
const orgId = new TerraformVariable(this, "orgId", {
type: "string",
description: "MongoDB Atlas Org ID",
sensitive: true,
});
// satisfy all the Atlas requirements before instantiating new cluster
new MongodbatlasProvider(this, "Atlas", {
publicKey: publicKey.value, // secret set via environment variable TF_VAR_publicKey
privateKey: privateKey.value, // secret set via environment variable TF_VAR_privateKey
});
const atlasProject = new Project(this, "newProject", {
name: "CDKTFProject1",
orgId: orgId.value, // set via environment variable TF_VAR_orgId
});
// create cluster resource
new Cluster(this, "newCluster1", {
projectId: atlasProject.id,
name: "atlasClusterCDK",
clusterType: "REPLICASET",
cloudBackup: false,
mongoDbMajorVersion: "5.0",
providerName: "TENANT",
backingProviderName: "GCP",
providerInstanceSizeName: "M0",
providerRegionName: "CENTRAL_US",
replicationSpecs: [
{
numShards: 1,
regionsConfig: [
{
electableNodes: 3,
priority: 7,
readOnlyNodes: 0,
regionName: "CENTRAL_US",
},
],
},
],
});
}
}
const app = new App();
new MyStack(app, "cdktf-gcp-mongodbatlas");
app.synth();
In this case we're using the free tier M0 Sandbox cluster, which is only recommended for development or testing purposes and doesn't support backups. Also keep in mind we are referencing the Atlas Region associated with the respective Google Cloud Region, not the GCP region name directly.
You can read more about that here: (https://www.mongodb.com/docs/atlas/reference/google-gcp/#std-label-google-gcp)
CDKTF Best Practices for handling secrets: use the TerraformVariable construct, Terraform uses the values in TerraformVariable directly at execution time, so CDKTF does not write them to the synthesized cdktf.json file. To pass a Terraform variable through environment variables, name the environment variable TF_VAR_NAME - 'NAME' being a placeholder for your variable name.
Environment variables that need to be set in the execution environment:
- TF_VAR_publicKey
- TF_VAR_privateKey
- TF_VAR_orgId
You can now feel free to run cdktf plan and cdktf deploy in order to get the base setup going, or instead choose to do it at the end once we wrap up all the other resource additions.
Let's go back and update the existing Cluster resource by assigning it to a constant in main.ts, then adding the admin database user to that cluster.
// create cluster resource
const atlasCluster = new Cluster(this, "newCluster1", {
projectId: atlasProject.id,
name: "atlasClusterCDK",
clusterType: "REPLICASET",
cloudBackup: false,
mongoDbMajorVersion: "5.0",
providerName: "TENANT",
backingProviderName: "GCP",
providerInstanceSizeName: "M0",
providerRegionName: "CENTRAL_US",
replicationSpecs: [
{
numShards: 1,
regionsConfig: [
{
electableNodes: 3,
priority: 7,
readOnlyNodes: 0,
regionName: "CENTRAL_US",
},
],
},
],
});
const adminPassword = new TerraformVariable(this, "adminPassword", {
type: "string",
description: "MongoDB Atlas Cluster DB Admin",
sensitive: true,
});
// add admin user to cluster
new DatabaseUser(this, "adminUser", {
username: "cdktf-adminuser",
password: adminPassword.value, // secret set via environment variable TF_VAR_adminPassword
projectId: atlasProject.id,
authDatabaseName: "admin",
roles: [
{
roleName: "readAnyDatabase",
databaseName: "admin",
},
],
scopes: [
{
name: atlasCluster.name,
type: "CLUSTER",
},
],
});
Environment variables that need to be set in the execution environment:
- TF_VAR_adminPassword
Finally, let's add the remaining network access piece to our stack in main.ts:
const userNetwork = new TerraformVariable(this, "userNetwork", {
type: "string",
description: "MongoDB Atlas Project IP access",
sensitive: false,
});
new ProjectIpAccessList(this, "projectnetworkAccess", {
projectId: atlasProject.id,
cidrBlock: userNetwork.value, // secret set via environment variable TF_VAR_userNetwork
});
Last but not least, define the one remaining env variable with your network CIDR.
Environment variables that need to be set in the execution environment:
- TF_VAR_userNetwork
cdktf plan
cdktf deploy
And voila! We are now ready to start developing against our MongoDB Atlas cluster from our machine. Bon coding.