Deploy Infrastructure With CDKTF

Nate Vick - August 21, 2021

CDK or Cloud Development Kit came out of AWS in 2018 as a way to write Infrastructure as Code in software languages used day-to-day by developers (JavaScript/TypeScript, Python, Java, C#, and soon Go). Since its release, a community has built up around it, and new flavors have arrived. AWS CDK, CDKTF, and CDK8s are all based on the same core, but compile to different formats. AWS CDK compiles to CloudFormation, while CDKTF compiles to Terraform compatible JSON, and CDK8s compiles to Kubernetes config.

In this post, we will walk through how to use CDKTF with DigitalOcean's Terraform provider. A Terraform provider is a "plugin" for Terraform to interact with remote systems. In this case, the provider is created and maintained by DigitalOcean. We will share a few examples of creating a Digital Ocean Project, a VPC, a Postgres Database, and a Droplet.

Prereqs and Install

To use cdktf you will need the following packages installed:

  • Terraform ≥ 0.12
  • Node.js ≥ 12.16
  • Yarn ≥ 1.21

To install cdktf, we will use npm install

$ npm install --global cdktf-cli

You will also need to create a personal access token on Digital Ocean if you would like to deploy this config.

Create and Initialize the example project

First let's make a directory and change into it.

$ mkdir cdk-do-example && cd cdk-do-example

Initialize the project with the init command. In general, I use the --local flag, so all state is stored locally.

$ cdktf init --template=typescript --local

You will be prompted for Project Name and Description but defaults are fine.

The last step of setting up the project is to add Terraform providers to the cdktf.json. Open the project in your editor and let's add the DigitalOcean provider to it as follows.

{
  "language": "typescript",
  "app": "npm run --silent compile && node main.js",
  "terraformProviders": [
    "digitalocean/digitalocean@~> 2.9"
  ],
  "terraformModules": [],
  "context": {
    "excludeStackIdFromLogicalIds": "true",
    "allowSepCharsInLogicalIds": "true"
  }
}

Install Dependencies

Run cdktf get to download the dependencies for using DigitalOcean with Typescript.

$ cdktf get
Generated typescript constructs in the output directory: .gen

Let's Write Some TypeScript

Open main.ts in your editor and let's start by creating a DO Project.

import { Construct } from 'constructs'
import { App, TerraformStack, Token } from 'cdktf'
import { DigitaloceanProvider } from './.gen/providers/digitalocean'
import { Project } from './.gen/providers/digitalocean'

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name)

    new DigitaloceanProvider(this, 'digitalocean', {
      token: Token.asString(process.env.DO_TOKEN) 
    })

    new Project(this, 'example-project', {
      name: 'Example Rails Project'
    })

  }
}

const app = new App()
new MyStack(app, 'cdk-do-example')
app.synth()

You will create a new DigitaloceanProvider and pass in the environment variable assigned to your DO personal access token. Next, we create the Project, which has a required key of name.

Optionally, as a test of the code above, run cdktf deploy to deploy your CDKTF project. The cli will ask if you want to make the changes listed under Resources. Type 'yes', then once it finishes, you will see a green check next to the Resources it successfully created.

cli prompt: Agree to deployment

successful deployment: First deployment

Now we will add a VPC, a Postgres Database, and a Droplet. Once those examples are in, we will tie it all together before deploying it again.

Create a VPC

import { Construct } from 'constructs'
import { App, TerraformStack, Token } from 'cdktf'
import { DigitaloceanProvider, Droplet, Vpc } from './.gen/providers/digitalocean'
import { Project } from './.gen/providers/digitalocean'

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name)

    new DigitaloceanProvider(this, 'digitalocean', {
      token: Token.asString(process.env.DO_TOKEN) 
    })
    ...more code above

    new Vpc(this, 'example-vpc', {
      name: 'example-vpc',
      region: 'sfo3'
    })

  }
}

const app = new App()
new MyStack(app, 'cdk-do-example')
app.synth()

*Note: If you do not have VPC in your account already this will become the default VPC which will not be deleted when cleaning up the CDKTF Project.

Create a Postgres Database

import { Construct } from 'constructs'
import { App, TerraformStack, Token } from 'cdktf'
import { DigitaloceanProvider, Droplet, Vpc, DatabaseCluster, DatabaseUser, DatabaseDb } from './.gen/providers/digitalocean'
import { Project } from './.gen/providers/digitalocean'

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name)

    new DigitaloceanProvider(this, 'digitalocean', {
      token: Token.asString(process.env.DO_TOKEN) 
    })
    ...more code above

    const postgres = new DatabaseCluster(this, 'example-postgres', {
      name: 'example-postgres',
      engine: 'pg',
      version: '13',
      size: 'db-s-1vcpu-1gb',
      region: 'sfo3',
      nodeCount: 1
    })

    new DatabaseUser(this, 'example-postgres-user', {
      clusterId: `${postgres.id}`,
      name: 'example'
    })

    new DatabaseDb(this, 'example-postgres-db', {
      clusterId: `${postgres.id}`,
      name: 'example-db'
    })

  }
}

const app = new App()
new MyStack(app, 'cdk-do-example')
app.synth()

Notice we assigned the DatabaseCluster to a variable const postgres. We then use the variable to create the DatabaseUser and DatabaseDb on that cluster.

Create a Droplet

import { Construct } from 'constructs'
import { App, TerraformStack, Token } from 'cdktf'
import { DigitaloceanProvider, Droplet, Vpc, DatabaseCluster, DatabaseUser, DatabaseDb } from './.gen/providers/digitalocean'
import { Project } from './.gen/providers/digitalocean'

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name)

    new DigitaloceanProvider(this, 'digitalocean', {
      token: Token.asString(process.env.DO_TOKEN) 
    })
    ...more code above

    new Droplet(this, 'example-droplet', {
      name: 'example-droplet',
      size: 's-1vcpu-1gb',
      region: 'sfo3',
      image: 'ubuntu-20-04-x64'
    })

  }
}

const app = new App()
new MyStack(app, 'cdk-do-example')
app.synth()

Let's Put It All Together

If you run cdktf deploy now, it would create everything, but nothing created would be put into the Digital Ocean project or the VPC we create. Let's do that now.

import { Construct } from 'constructs'
import { App, TerraformStack, Token } from 'cdktf'
import { DigitaloceanProvider, Droplet, Vpc} from './.gen/providers/digitalocean'
import { DatabaseCluster, DatabaseUser, DatabaseDb } from './.gen/providers/digitalocean'
import { Project, ProjectResources } from './.gen/providers/digitalocean'

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name)

    new DigitaloceanProvider(this, 'digitalocean', {
      token: Token.asString(process.env.DO_TOKEN) 
    })

    const project = new Project(this, 'example-project', {
      name: 'Example Rails Project'
    })

    const vpc = new Vpc(this, 'example-vpc', {
      name: 'example-vpc',
      region: 'sfo3'
    })

    const postgres = new DatabaseCluster(this, 'example-postgres', {
      name: 'example-postgres',
      engine: 'pg',
      version: '13',
      size: 'db-s-1vcpu-1gb',
      region: 'sfo3',
      nodeCount: 1,
      privateNetworkUuid: vpc.id
    })

    new DatabaseUser(this, 'example-postgres-user', {
      clusterId: `${postgres.id}`,
      name: 'example'
    })

    new DatabaseDb(this, 'example-postgres-db', {
      clusterId: `${postgres.id}`,
      name: 'example-db'
    })

    const droplet = new Droplet(this, 'example-droplet', {
      name: 'example-droplet',
      size: 's-1vcpu-1gb',
      region: 'sfo3',
      image: 'ubuntu-20-04-x64',
      vpcUuid: vpc.id
    })

    new ProjectResources(this, 'example-project-resources', {
      project: project.id,
      resources: [
        postgres.urn,
        droplet.urn
      ],
      dependsOn: [ postgres, droplet ]
    })
  }
}

const app = new App()
new MyStack(app, 'cdk-do-example')
app.synth()

We start by assigning the project, VPC, and droplet to variables. In the DatabaseCluster definition, we add privateNetworkUuid: [vpc.id](http://vpc.id) to place the database in our newly created VPC. Similarly, on the Droplet definition, we place it in the VPC, by adding vpcUuid: vpc.id.

Lastly, create a new ProjectResource to assign other resources to the Digital Ocean project. In this small example, we will assign the database and droplet to the project using the urn for each resource. We will wait to assign those until both are created using a Terraform helper dependsOn.

With all of that in place you can deploy again. The database and droplet creation take a bit, so be patient. 🙂 Once it has finished, check your Digital Ocean Dashboard to see everything created and ready for use.

Full Example Deployed

*Make sure you run cdktf destroy to remove these resources from your account or you will be charged by Digital Ocean.*

Things To Know

CDKTF is still a young project, so it is changing fast, has limited documentation, and you can run into unclear errors.

There are few things that help with the limited documentation. You can read the provider documentation on the Terraform registry site and use it as a guide. Also, using a language like Typescript with VSCode there are a lot of code hints, hover info, and signature information. The gif below is an example of what is shown in VSCode when you hover on the problem. Note the missing properties for DropletConfig.

Typescript Annotations

When you run into unclear errors, prepend CDKTF_LOG_LEVEL=debug to the deploy and/or destroy commands to get very verbose output.

Wrapping Up

In this post, we used CDKTF to create some basic example resources on Digital Ocean which gives you a good primer to build more complex infrastructure in a language of your choice. You can find the code from this post in this repo. If you would like to chat more about CDK or infrastructure as code you can ping me on Twitter @natron99.

Nate Vick

Nate is partner and COO at Hint. He keeps the wheels turning, so to speak. In his free time he enjoys spending time with his wife and kids, hiking, and exploring new technology.

  
  
  

Ready to Get Started?

LET'S CONNECT