A Terraform CDK Construct which doubles as native Terraform Module
Tags: terraform, cdk, cdktf, devops
Published: 2020-07-27 11:12:31

Lou [raised the question](https://dev.to/loujaybee/should-you-use-typescript-to-write-terraform-the-terraform-cdk-cm) of Terraform Module interoperability in the Terraform CDK:

> Module Interoperability — It seems the CDK will support regular Terraform modules, for code sharing. But it does remain to be seen if module sharing can be reversed. Can a CDK project publish a module which is then consumed by HCL? If you’d have to learn HCL to write and share a module, that mostly defeats the point of using the CDK in the first place.

That's a great question, and while I was confident this is something which could be done today already, I thought this would be a good example to actually build out. So, that's what I did and I'd like to share with you.

## General Concept

The Terraform CDK is synthesizing from code to HCL compatible JSON. This makes it less a question of *if* it's possible to, but more like a question of *how* to organize the code and how to distribute it.

### Resource vs TerraformResource

The concept I came up with includes a [custom construct](https://github.com/skorfmann/cdktf-hybrid-module/blob/7a84cbea62fbc3c3b7e92c00d75fcaad495cf29b/packages/cdktf-hybrid-module/lib/construct.ts), which is sort of the equivalent of a native Terraform module in the Terraform CDK. That's just a Typescript class, which wraps other Terraform constructs (an EC2 `Instance` in this case). It could contain more constructs of course, they could be nested, whatever you can imagine - it's all just Typescript in the end.

```ts
import { Construct } from 'constructs';
import { Resource } from 'cdktf';
import { Instance } from '../imports/providers/aws'

export interface CustomInstanceProps {
  instanceType?: string;
  tags?: {[key: string]: string};
}

export class CustomInstance extends Resource {
  public readonly instance: Instance;

  constructor(scope: Construct, name: string, props?: CustomInstanceProps) {
    super(scope, name);

    const { tags, instanceType = "t3.nano" } = props || {};

    this.instance = new Instance(this, 'ubuntu2', {
      ami: "ami-0ff8a91507f77f867",
      availabilityZone: "us-east-1a",
      instanceType,
      tags
    })
  }
}
```

In contrast to the generated `Instance` class, the `CustomInstance` class extends [Resource](https://github.com/hashicorp/terraform-cdk/blob/5becfbc699180adfe920dec794200bbf56dda0a7/packages/cdktf/lib/resource.ts) and not [TerraformResource](https://github.com/hashicorp/terraform-cdk/blob/master/packages/cdktf/lib/terraform-resource.ts). While `CustomInstance` is still a node in the [Constructs](https://github.com/aws/constructs) tree, the `CustomInstance` class itself will be skipped during synth. Only the actual `TerraformResource` classes will be rendered down to HCL compatible JSON. It's really just a container for actual Terraform resources.

### Distribution

#### NPM Package

Since our `CustomInstance` is a Typescript class, distributing this as a NPM package is straightforward. Make sure to setup the following keys in your `package.json` and off you go:

```json
  "main": "path/to/your/construct.js",
  "types": "path/to/your/construct.d.ts",
  "peerDependencies": {
    "cdktf": "^0.0.12",
    "constructs": "^3.0.0"
  },
```

Declaring `peerDependencies` and not `dependencies` is the important point here. Otherwise, you'd likely end up in dependency hell.

#### Terraform Module

In order to distribute this as a Terraform module, this should be synthesized to JSON somehow. Let's build a Stack without a provider and that'll pretty much be synthesized to the equivalent of a Terraform module.

```ts
import { Construct } from 'constructs';
import { App, TerraformStack, TerraformOutput, Token } from 'cdktf';
import { CustomInstance } from './construct'

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

    const custom = new CustomInstance(this, 'Custom')

    new TerraformOutput(this, 'arn', {
      value: custom.instance.arn
    })
  }
}
```

Rather than ignoring the the synthesized output in Git, let's [commit this](https://github.com/skorfmann/cdktf-hybrid-module/tree/master/packages/cdktf-hybrid-module/module) and configure `cdktf` to use a nicer folder name (`module` in this case) for its output:

```json
{
  "language": "typescript",
  "app": "npm run --silent compile && node lib/module.js",
  "terraformProviders": [
    "aws@~> 2.0"
  ],
  "codeMakerOutput": "imports",
  "output": "module"
}
```

For the native Terraform module use-case it would be nice to omit the [stack traces](https://github.com/skorfmann/cdktf-hybrid-module/blob/7a84cbea62fbc3c3b7e92c00d75fcaad495cf29b/packages/cdktf-hybrid-module/module/cdk.tf.json#L19-L32) in the generated JSON, since that'll change depending on where it's build. A little bit of `jq` could be helpful here

```
cat ./cdktf.out/cdk.tf.json | jq 'walk(if type == "object" then with_entries(select(.key | test("\/\/") | not)) else . end)'
```

Mid / long term, native support would be better though. Check out this [open issue here](https://github.com/hashicorp/terraform-cdk/issues/251).

##### Input Variables

[Terraform Input Variables](https://www.terraform.io/docs/configuration/variables.html) are not natively supported by the Terraform CDK yet. There's an [open issue](https://github.com/hashicorp/terraform-cdk/issues/249) to change this. However, that's a case where [escape hatches](https://cdk.tf/escape-hatch) come in handy:

```ts
const stack = new MyStack(app, 'cdktf-hybrid-module');

// See issue linked above, this will be natively supported
stack.addOverride('variable', {
  tags: {
    description: "Tags for the instance",
    type: "map(string)"
  },
  instance_type: {
    description: "Instance type",
    type: "string"
  }
})
```

From here on, it's really useable as any other Terraform module:

```hcl
module "instance" {
  source = "github.com/skorfmann/cdktf-hybrid-module//packages/cdktf-hybrid-module/module"

  instance_type = "t3.nano"

  tags = {
    "CDKTF" = "IS AWESOME"
  }
}
```

#### Bonus: Terraform Module via NPM

One thing I really like about NPM: It doesn't make any assumptions about what you're intending to ship. Let's leverage this, to use the native Terraform module via NPM :)

```
npm init -y
npm install cdktf-hybrid-module
```

And then just reference it from `node_modules`.

```hcl
module "instance" {
  source = "./node_modules/cdktf-hybrid-module/module"

  instance_type = "t3.nano"

  tags = {
    "CDKTF" = "IS AWESOME"
  }
}
``` 

That could be a way, to get dependency management for native Terraform modules via NPM. I'm certainly not the first one who had this idea, I'm pretty sure I saw this in other blog posts as well. 

I think this could make sense in complex scenarios, where dependency management is important and manually managing this becomes a burden.

## Current limitations

As mentioned a few times, there are a few usability gaps at the moment: 

- Outputs are already fully supported by `cdktf` but due to the random naming a bit hard to use. There' an [open issue](https://github.com/hashicorp/terraform-cdk/issues/247) to address this
- Variables aren't natively supported in `cdktf` yet, but can still be done with [escape hatches](https://cdkt.tf/escape-hatche). There's an [open issue](https://github.com/hashicorp/terraform-cdk/issues/249)
- Since there aren't official prebuilt provider packages at the moment, this has to inline the [generated constructs](https://github.com/skorfmann/cdktf-hybrid-module/tree/7a84cbea62fbc3c3b7e92c00d75fcaad495cf29b/packages/cdktf-hybrid-module/imports/providers/aws). 

The last point is the biggest drawback at the moment from my point of view, but the work to improve this is underway - see this [open issue](https://github.com/hashicorp/terraform-cdk/issues/98).

## Conclusion

I think this demonstrates that hybrid CDK packages / Terraform modules are totally possible and have lots of future potential. Would love to hear what you think about it!

Check out the entire example project as well to see all of this in full context.

{% github skorfmann/cdktf-hybrid-module no-readme %}

In the next post we'll build a Python package for our little `CustomInstance`, stay tuned!