Lou raised the question 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, 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.
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 and not TerraformResource. While CustomInstance
is still a node in the 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:
"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.
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 and configure cdktf
to use a nicer folder name (module
in this case) for its output:
{
"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 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.
Input Variables
Terraform Input Variables are not natively supported by the Terraform CDK yet. There's an open issue to change this. However, that's a case where escape hatches come in handy:
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:
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
.
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 to address this - Variables aren't natively supported in
cdktf
yet, but can still be done with escape hatches. There's an open issue - Since there aren't official prebuilt provider packages at the moment, this has to inline the generated constructs.
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.
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.
skorfmann / cdktf-hybrid-module
A Terraform CDK Construct which is also usable as Terraform Module
In the next post we'll build a Python package for our little CustomInstance
, stay tuned!
Top comments (0)