Deploy Infrastructure With CDKTF
Tags: terraform, devops
Published: 2021-09-07 20:19:54

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](https://docs.digitalocean.com/reference/api/create-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.

```bash
$ 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjekjujx94rx8dkjzjki.png)

successful deployment:
![First deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufzx6hnfwgnv6xmvdkkf.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctax9tuohfejr36iezuu.png)

**\*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](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs) 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](https://media.giphy.com/media/Ae5Z300UKAWhqZiHfc/source.gif)

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](https://github.com/hintmedia/cdk-do-example). If you would like to chat more about CDK or infrastructure as code you can ping me on Twitter [@natron99](https://twitter.com/natron99).