CDK for Terraform with TypeScript
Introduction to CDKTF
Providers and Resources
In this guide, we explore several essential CDKTF concepts: Providers and Resources, Outputs, and Constructs. We begin with Providers and Resources.
Arthur embarks on his CDKTF journey to automate the creation of common project files such as README.md, package.json, and .gitignore. To achieve this, he utilizes the file resource offered by CDKTF. Arthur opens his lab environment and starts his next CDKTF exercise.
He then opens a new terminal to inspect the initial project structure, which already includes some sample code:
import { Construct } from 'constructs';
import { App, TerraformStack } from 'cdktf';
import { provider } from 'cdktf/provider-local';
import * as path from 'path';
import { file } from 'cdktf/provider-local'; // Use the 'file' construct directly from 'provider-local'
import { ProjectFolder } from './constructs/ProjectFolder';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Initialize the local provider
new provider.LocalProvider(this, 'local', {});
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
}
}
For those following along in "hard mode," Arthur deletes the initial code and starts fresh with a simplified version:
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
new ProjectFolder(this, 'project-folder', {
projectName,
projectDirectory,
});
// Create the .gitignore file independently
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
root in ~/code via ⬢ v20.17.0 on ⬢ (us-east-1)
He then removes the folder created during cdktf init
and runs yarn install
to fetch the project dependencies.
The simplified project code now appears as follows:
import { App } from 'cdktf';
import { provider } from 'cdktf/provider-local';
import * as path from 'path';
import { ProjectFolder } from './constructs/ProjectFolder';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Create the .gitignore file independently
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
root in ~/code via 🦕 v20.17.0 on ⬢ (us-east-1)
After yarn install
completes, Arthur refreshes VS Code to recognize the new dependencies. He then performs a quick "Hello World" test by deploying a simple Terraform output:
import { Construct } from 'constructs';
import { App, TerraformOutput, TerraformStack } from 'cdktf';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new TerraformOutput(this, 'lets-go', { value: 'Lets go!' });
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
root in ~/code via 🦕 v20.17.0 on 🌏 (us-east-1)
> yarn cdktf deploy
After initializing the new project, Arthur confirms that both the stack (named "cdktf-project-builder") and its parent app are configured correctly. The deployment output confirms that the TypeScript code compiles successfully into Terraform configuration:
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new TerraformOutput(this, 'lets-go', { value: 'Lets go!' });
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
lets-go = "Lets go!"
cdktf-project-builder
lets-go = Lets go!
root in ~/code via v20.17.0 on (us-east-1) took 20s
Arthur then inspects the cdkout
folder to review the stack and the generated Terraform configuration files. An example of the synthesized Terraform state file is shown below:
{
"version": 3,
"terraform_version": "1.9.5",
"backend": {
"type": "local",
"config": {
"path": "/root/code/terraform.cdktf-project-builder.tfstate",
"workspace_dir": null
}
},
"hash": 1453352099
}
cdktf-project-builder
cdktf-project-builder
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
lets-go = "Lets go!"
root in ~/code via v20.17.0 on (us-east-1) took 20s
With the basic deployment verified, Arthur proceeds to leverage a local file resource. To work with local files, he needs to add the appropriate provider. Unlike a native Terraform project (where you would simply run terraform init
after adding the provider), in CDKTF you must declare the dependency explicitly.
First, Arthur installs the CDKTF core library:
yarn add @cdktf
Next, he installs the local provider:
yarn add @cdktf/provider-local
This provider enables CDKTF to manage local file resources. Once the dependency is in place, Arthur updates his code to create a README file:
import { Construct } from 'constructs';
import { App, TerraformStack } from 'cdktf';
import { provider, file } from '@cdktf/provider-local';
import * as path from 'path';
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Initialize the local provider
new provider.LocalProvider(this, 'local', {});
// Define the project folder on the file system
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
const basePath = `${projectDirectory}/${projectName}`;
// Create a README file with sample content
new file.File(this, 'readme-file', {
filename: `${basePath}/README.md`,
content: `# ${projectName}\n\nThis is the ${projectName} project.`,
});
}
}
const app = new App();
new MyStack(app, 'cdktf-project-builder');
app.synth();
When Arthur runs yarn cdktf deploy
, the output confirms the creation of the file resource:
YN00085 - cdktf/provider-local@npm:10.1.0
Completed in 0s 728ms
Post-resolution validation
Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for detail
root in ~/code via v20.17.0 on (us-east-1) took 5s
yarn cdktf deploy
The synthesis process converts the TypeScript code into Terraform configuration. If an error arises (such as from TypeScript's "no unused locals" rule), Arthur can either remove the unused variable or adjust the TypeScript configuration accordingly.
After running cdktf synth
followed by cdktf deploy
, CDKTF generates a Terraform plan indicating that one resource will be added (the local file). The output looks similar to:
Plan: 1 to add, 0 to change, 0 to destroy.
After approving the plan, Terraform creates the file, confirming the successful deployment:
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
cdktf-project-builder local_file.readme-file: Creating...
cdktf-project-builder local_file.readme-file: Creation complete after 0s [id=43fe3377e9436957407c5674078d3878fffd890]
cdktf-project-builder
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Key Concepts
- A construct represents any resource or infrastructure component in your construct tree.
- A provider manages the connection and configuration for resources (e.g.,
provider.LocalProvider
). - A resource (such as
file.File
) defines a local file, similar to HashiCorp Configuration Language (HCL) in native Terraform projects.
The CDKTF code is ultimately synthesized into Terraform configurations. For instance, the Terraform configuration for the local file resource includes parameters like the filename and content. An example snippet might resemble:
{
"terraform": {
"backend": {
"local": {
"path": "/root/code/terraform.cdktf-project-builder.tfstate"
}
},
"required_providers": {
"local": {
"source": "hashicorp/local",
"version": "2.5.2"
}
}
}
}
Running yarn cdktf synth
creates the necessary Terraform configuration, and yarn cdktf deploy
applies the configuration after displaying a detailed plan.
If Arthur decides to modify the README file content, Terraform will replace the file resource imperatively. For example, updating the README content:
// Create project folder
const projectDirectory = path.join(process.env.INIT_CWD!, './authors-projects');
const projectName = 'project-1';
const basePath = `${projectDirectory}/${projectName}`;
// Update the README file content
new file.File(this, 'readme-file', {
filename: `${basePath}/README.md`,
content: `# ${projectName}\n\nThis is the ${projectName} project!`,
});
Upon deployment, Terraform indicates that the resource must be replaced:
~ resource "local_file" "readme-file" {
~ content = <<-EOT # forces replacement
# project-1
This is the project-1 project!
EOT
}
Note on Imperative Management
Local files are managed imperatively by Terraform. Any modification to the file content will trigger a resource replacement. For cloud resources, such as an AWS S3 bucket, updates might not require replacement, and lifecycle rules can be applied using the lifecycle
configuration.
For example, ignoring changes to the file content can be configured as follows:
new file.File(this, 'readme-file', {
filename: `${basePath}/README.md`,
content: `# ${projectName}\n\nThis is the ${projectName} project!!!!!`,
lifecycle: { ignoreChanges: ['content'] },
});
After deploying the above, subsequent redeployments show:
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Changing the resource's logical ID (e.g., renaming it from readme-file
to readme-file-2
) results in the destruction of the old resource and creation of a new one.
To add a package.json
file along with the README, Arthur extends his code:
new file.File(this, 'package-json-file', {
filename: `${basePath}/package.json`,
content: JSON.stringify(
{
name: projectName,
version: '1.0.0',
main: 'index.js',
scripts: {
start: 'node index.js',
},
},
null,
2
),
});
After running yarn cdktf deploy
, Terraform confirms that the new file resource has been added:
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Verifying the created package.json
confirms its content:
{
"name": "project-1",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
}
}
cdktf-project-builder local_file.package-json-file: Creation complete after 0s [id=...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Terraform State and Resource Updates
Consider the following points regarding Terraform state and resource updates:
Where is the Terraform state stored?
By default, Terraform stores state locally. You can find atf.state
file within thecdktf.out
folder of your stack, which contains details such as the filename and file permissions.What happens when the README file content is modified?
Any change to the README file content forces a replacement of the resource, unless you configurelifecycle: { ignoreChanges: ['content'] }
.What occurs if the logical ID for the README file is changed?
Changing the logical ID removes the existing resource from the construct tree and creates a new one, prompting the destruction of the old file and the creation of a new file.How do you add a
package.json
file alongside the README file?
As demonstrated above, you can introduce an additional file resource specifying both the filename and content forpackage.json
.
To deploy your changes, execute:
yarn cdktf synth
yarn cdktf deploy
CDKTF first synthesizes your TypeScript code into Terraform configuration and then applies the generated plan, mirroring the native Terraform workflow.
This concludes the section on Providers and Resources. Next, we will delve into Outputs and explore how they integrate with the construct tree in CDKTF.
Watch Video
Watch video content