CDK for Terraform with TypeScript
Introduction to CDKTF
Constructs
In this lesson, we explore Constructs in CDKTF, which enable you to encapsulate and reuse resource definitions. Constructs allow you to group resources into reusable components. In our example, Arthur creates a "Project Folder" construct to manage his project setups dynamically, offering an alternative to traditional Terraform modules.
Below is the boilerplate code for Arthur’s Project Folder construct using modern TypeScript features:
import { Construct } from 'constructs';
import { file } from '@cdktf/provider-local';
interface ProjectFolderProps {
readonly projectName: string;
readonly projectDirectory: string;
}
export class ProjectFolder extends Construct {
constructor(scope: Construct, id: string, props: ProjectFolderProps) {
super(scope, id);
const { projectName, projectDirectory } = props;
// Reusable code...
}
}
Understanding TypeScript Concepts
The extends Keyword
The extends
keyword creates a new class based on an existing one. In our example, ProjectFolder
extends the Construct
class, meaning it inherits properties and methods from Construct
while allowing customization specific to your project.
The super Method
Inside the constructor of the child class, calling super(scope, id)
initializes the inherited properties from the parent class. Passing the correct initialization parameters (here, scope
and id
) ensures that the construct is set up properly.
Destructuring in TypeScript
The following line uses destructuring to extract values from the props
object:
const { projectName, projectDirectory } = props;
This is equivalent to:
const projectName = props.projectName;
const projectDirectory = props.projectDirectory;
This syntax keeps your code concise and clean.
Integrating the Construct into Your Project
It's common to organize your constructs within a dedicated folder (for example, a folder named "constructs") and name the file after the exported class (e.g., ProjectFolder.ts
). The example below shows how to use the construct in a Terraform stack:
import { Construct } from 'constructs';
import { App, TerraformOutput, TerraformStack } from 'cdktf';
import { file, provider } from '@cdktf/provider-local';
import * as path from 'path';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Initialise the local provider
new provider.LocalProvider(this, 'local', {});
// Create project folder
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
const basePath = `${projectDirectory}/${projectName}`;
// Add a README file
const readMeFile = new file.File(this, 'readme-file', {
content: `This is the project-1 project`
});
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
When using Visual Studio Code with TypeScript, you can quickly import any missing dependencies by using the shortcut (Command Period on macOS or Control Period on Windows).
Moving Resources into the Construct
Rather than adding resources directly in your stack, you can encapsulate them inside your construct. The following updated version of the Project Folder construct includes a README file creation:
import { Construct } from 'constructs';
import { file } from '@cdktf/provider-local';
interface ProjectFolderProps {
readonly projectName: string;
readonly projectDirectory: string;
}
export class ProjectFolder extends Construct {
constructor(scope: Construct, id: string, props: ProjectFolderProps) {
super(scope, id);
const { projectName, projectDirectory } = props;
const basePath = `${projectDirectory}/${projectName}`;
// Add a README file
const readMeFile = new file.File(this, 'readme-file', {
filename: `${basePath}/README.md`,
content: `# ${projectName}\n\nThis is the ${projectName} project`,
});
}
}
Adding the Construct to the Stack
Instantiate your construct within your stack code as follows:
import * as path from 'path';
import { TerraformStack, App, TerraformOutput } from 'cdktf';
import { provider } from '@cdktf/provider-local';
import { ProjectFolder } from './constructs/ProjectFolder';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Initialise the local provider
new provider.LocalProvider(this, 'local', {});
// Create project folder
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
new ProjectFolder(this, 'project-folder', {
projectName,
projectDirectory,
});
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
After running the deployment command (for example, yarn cdktf deploy
), Terraform will create or update resources as defined within your construct.
Tip
Using constructs allows you to keep your infrastructure code modular and maintainable, reducing repetition and simplifying updates.
Exposing Output from the Construct
To reference a resource created inside your construct (like the README file content) in your main stack, expose properties as read-only attributes. Update your construct as follows:
import { Construct } from 'constructs';
import { file } from '@cdktf/provider-local';
interface ProjectFolderProps {
readonly projectName: string;
readonly projectDirectory: string;
}
export class ProjectFolder extends Construct {
// Expose the readMeFile so it can be accessed externally.
readonly readMeFile: file.File;
constructor(scope: Construct, id: string, props: ProjectFolderProps) {
super(scope, id);
const { projectName, projectDirectory } = props;
const basePath = `${projectDirectory}/${projectName}`;
// Create the README file and assign it to the readMeFile property.
this.readMeFile = new file.File(this, 'readme-file', {
filename: `${basePath}/README.md`,
content: `# ${projectName}\n\nThis is the ${projectName} project`,
});
}
}
Now, update your stack to use this output:
import * as path from 'path';
import { TerraformStack, App, TerraformOutput } from 'cdktf';
import { provider } from '@cdktf/provider-local';
import { ProjectFolder } from './constructs/ProjectFolder';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Initialise the local provider
new provider.LocalProvider(this, 'local', {});
// Create project folder and capture the instance
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
const projectFolder = new ProjectFolder(this, 'project-folder', {
projectName,
projectDirectory,
});
// Expose the README file content as a Terraform output.
new TerraformOutput(this, 'readMeContent', {
value: projectFolder.readMeFile.content,
});
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
After synthesizing, the output will display something similar to:
readMeContent = # project-1
This is the project-1 project
This approach allows you to reference outputs from one construct and pass them into another easily.
Final Thoughts
Constructs in CDKTF offer a flexible, programmable alternative to native Terraform modules. With constructs, you can encapsulate resource creation logic into reusable components, ensuring clean, modular, and maintainable code. Leveraging modern TypeScript features such as class inheritance and destructuring can further simplify your infrastructure code.
Additional Resource
Learn more about Terraform Modules to further enhance your infrastructure as code practices.
Watch Video
Watch video content