Terraform
Stacks
A stack represents a collection of infrastructure that CDK for Terraform (CDKTF) synthesizes as a dedicated Terraform configuration. Stacks allow you to separate the state management for multiple environments within an application.
Note: Stacks in CDKTF are different from the Terraform stacks concept announced at HashiConf 2023. Terraform stacks are a configuration layer that simplifies provisioning and managing resources at scale by controlling cross-configuration dependencies between Terraform modules. Refer to Terraform stacks, explained in the HashiCorp blog for additional information. Hands-on: Try the Deploy Applications with CDK for Terraform tutorial.
Scope
You can instantiate the same resource multiple times throughout your infrastructure. For example, you may want to create multiple S3 Buckets with different configurations. Instances that share the same stack
parent element are considered to be part of the same scope. You must set a different name
property for each instance to avoid naming conflicts.
Refer to the constructs documentation for more details and an example.
Single Stack
The following example generates a single Terraform configuration in the configured output folder. When you run cdktf synth
, the synthesized Terraform configuration will be in the folder cdktf.out/stacks/a-single-stack
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/aws-provider";
import { Instance } from "@cdktf/provider-aws/lib/instance";
class MySingleStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", {
region: "us-east-1",
});
new Instance(this, "Hello", {
ami: "ami-2757f631",
instanceType: "t2.micro",
});
}
}
const singleStackApp = new App();
new MySingleStack(singleStackApp, "a-single-stack");
singleStackApp.synth();
Multiple Stacks
Hands-on: Try the Deploy Multiple Lambda Functions with TypeScript tutorial. This tutorial guides you through a multi-stack application.
You can specify multiple stacks in your application. For example, you may want a separate configuration for development, testing, and production environments.
The following example synthesizes multiple Terraform configurations in the configured output folder.
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/aws-provider";
import { Instance } from "@cdktf/provider-aws/lib/instance";
interface MyMultiStackConfig {
environment: string;
region?: string;
}
class MyMultiStack extends TerraformStack {
constructor(scope: Construct, id: string, config: MyMultiStackConfig) {
super(scope, id);
const { region = "us-east-1" } = config;
new AwsProvider(this, "aws", {
region,
});
new Instance(this, "Hello", {
ami: "ami-2757f631",
instanceType: "t2.micro",
tags: {
environment: config.environment,
},
});
}
}
const multiStackApp = new App();
new MyMultiStack(multiStackApp, "multiple-stacks-dev", {
environment: "dev",
});
new MyMultiStack(multiStackApp, "multiple-stacks-staging", {
environment: "staging",
});
new MyMultiStack(multiStackApp, "multiple-stacks-production-us", {
environment: "production",
region: "us-east-1",
});
new MyMultiStack(multiStackApp, "multiple-stacks-production-eu", {
environment: "production",
region: "eu-central-1",
});
multiStackApp.synth();
Running cdktf synth
produces the following synthesized stacks.
$ cdktf list
Stack name Path
multiple-stacks-dev cdktf.out/stacks/multiple-stacks-dev
multiple-stacks-staging cdktf.out/stacks/multiple-stacks-staging
multiple-stacks-production-us cdktf.out/stacks/multiple-stacks-production-us
multiple-stacks-production-eu cdktf.out/stacks/multiple-stacks-production-eu
To deploy and destroy multiple stacks at once, either specify multiple stacks in the cdktf deploy
and cdktf destroy
command or use a wild card glob (e.g., cdktf deploy '*-production'
).
Refer to Best Practices for more details about when to create multiple stacks and how to structure them.
Cross-Stack References
When you reference resources from one stack in another stack, you can do so by exposing the resource in the source stack and referencing it in the target stack.
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/aws-provider";
import { Instance } from "@cdktf/provider-aws/lib/instance";
import { Vpc } from "./constructs/vpc";
import { DockerBackend } from "./constructs/docker-backend";
class VPCStack extends TerraformStack {
public vpc: Vpc;
constructor(scope: Construct, id: string, public region = "us-east-1") {
super(scope, id);
new AwsProvider(this, "aws", {
region,
});
this.vpc = new Vpc(this, "vpc", {});
}
}
interface BackendStackConfig {
region: string;
vpcId: string;
dockerImage: string;
}
class BackendStack extends TerraformStack {
constructor(scope: Construct, id: string, config: BackendStackConfig) {
super(scope, id);
const { region, vpcId, dockerImage } = config;
new AwsProvider(this, "aws", {
region,
});
new DockerBackend(this, "docker-backend", {
vpcId,
dockerImage,
});
}
}
const crossStackReferenceApp = new App();
const origin = new VPCStack(crossStackReferenceApp, "origin-stack");
new BackendStack(crossStackReferenceApp, "target-stack", {
region: origin.region,
vpcId: origin.vpc.id,
dockerImage: "org/my-image:latest",
});
crossStackReferenceApp.synth();
From a usage perspective it looks like we are accessing the id
value of vpc
from the origin-stack
instance of VpcStack
and then referencing it in the target-stack
instance of BackendStack
.
Accessing a value from a different stack causes the value to be exported as TerraformOutput
in the origin stack.
The value is then accessed through a TerraformRemoteState
in the target stack.
Both are automatically added to the respective stacks to make the process seemless.
When you are using HCP Terraform, each stack must be its own workspace. This means that you need to create a separate workspace for each stack and you need to set the permissions to allow access between the stacks.
Stack Dependencies
We add the stack dependencies in the cdktf.out/manifest.json
file for each stack under dependencies
.
By default a stack is dependant on another stack when the data used origins in that stack.
If you e.g. write this.allResources = Fn.concat([resourceFromStackA.items, resourceFromStackB.items])
in Stack C and use stackC.allResources
in Stack D, Stack D will be dependant on Stack A and B, but not C since that is not the origin of the data.
To make the dependency explicit, runstackD.addDependency(stackC)
.
If you want to keep the result of the function attached to one stack and save its state, create a Terraform Local value and expose it. The following example creates a TerraformLocal
.
this.allResources = new TerraformLocal(this, "merged_items", [
sourceStackA.instance.id,
sourceStackB.instance.id,
]);
The CLI will error if you deploy your application without first deploying the dependencies. It will also error if you try to destroy infrastructure without destroying the dependent stacks first. To remove these safeguards, add the --ignore-missing-stack-dependencies
to the deploy
and destroy
commands.
Migration from <= 0.2
Until version 0.2
, CDKTF only supported a single stack. For local state handling, CDKTF used a terraform.tfstate
in the project root folder. With version >= 0.3
, the local state file reflects the stack name it belongs to in its file name. When a terraform.tfstate
file is still present in the project root folder, it has to be renamed to match the schema terraform.<stack-name>.tfstate
manually.
Escape Hatch
For anything on the top-level terraform
block that is not natively implemented, use the stack escape hatch to define a configuration. For example, define remote backend using the addOverride
method in TypeScript.
Important: Escape hatches must not have empty arguments or objects, because CDKTF removes them from the synthesized JSON configuration.
The following example synthesizes a Terraform configuration with the remote
backend included in the terraform
block.
stack.addOverride("terraform.backend", {
local: null, // delete the default local backend
remote: {
organization: "test",
workspaces: {
name: "test",
},
},
});
The following configuration snippet shows the remote
backend configuration.
{
"terraform": {
"required_providers": {
"aws": "~> 2.0"
},
"backend": {
"remote": {
"organization": "test",
"workspaces": {
"name": "test"
}
}
}
}
}