Terraform
Refactoring
You may need to move resources between stacks as your CDK for Terraform (CDKTF) application grows in size and complexity. This guide demonstrates how to refactor your stacks. In general you can change the names of stacks freely without an effect on the synthesized code. If you change the name of a resource this might lead to a re-creation of the resource, especially if you move the resource from one stack to another.
Moving & Renaming Resources Within a Stack
You may want to rename a deployed resource in a stack, like the S3Bucket
in the following example.
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket";
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", {
region: "us-east-1",
});
// old S3Bucket definition
// new S3Bucket(this, "my-bucket", {
// bucketprefix: "my-bucket",
// });
// new S3Bucket definition
new S3Bucket(this, "new-bucket", {
bucketprefix: "my-bucket",
});
}
}
const app = new App();
new MyStack(app, "refactoring-example");
app.synth();
When you run a cdktf plan
, the CLI may display the following output:
refactoring-example # aws_s3_bucket.my-bucket will be destroyed
# (because aws_s3_bucket.my-bucket is not in configuration)
- resource "aws_s3_bucket" "my-bucket" {
...
refactoring-example # aws_s3_bucket.new-bucket (new-bucket) will be created
+ resource "aws_s3_bucket" "new-bucket" {
Terraform plans to destroy the old bucket and create a new one to replace it. The replacement can be harmful if there is already state in the resource, such as files in the bucket or data in the database.
To avoid this recreation behavior, use one of the move instance functions (moveTo
, moveToId
, moveFromId
) available on the resource.
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket";
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", {
region: "us-east-1",
});
// old S3Bucket definition
new S3Bucket(this, "my-bucket", {
bucketprefix: "my-bucket",
}).moveTo("new-s3-bucket");
// new S3Bucket definition
new S3Bucket(this, "new-bucket", {
bucketprefix: "my-bucket",
}).addMoveTarget("new-s3-bucket");
}
}
Performing Resource Moves
The moveTo
function is available on all resources and is used for relocating a resource to the location specified by the string target moveTarget
. To set a resource's moveTarget
, use the addMoveTarget
function that is present on the resource to move to. You can specify an arbitrary string for the moveTarget
, but it must be unique within your stack.
new S3Bucket(this, "test-bucket-move-to", {
bucket: "move-bucket-name",
}).addMoveTarget("move-s3");
new S3Bucket(this, "test-bucket-move-from", {
bucket: "move-bucket-name",
}).moveTo("move-s3");
After deployment, both the resource being moved and the move target on the destination resource can be removed.
Move Targets
A moveTarget
is accessible anywhere within the context of the stack where it is created, including the root of the stack and within a nested construct. This workflow does not support moving resources to a different stack.
new S3Bucket(this, "test-bucket-1", {
bucket: "test-move-bucket-name-1",
}).moveTo("move");
// Contains resource with moveTarget "move"
new NestedConstructToMoveTo(this, "construct-to-move-to");
Enabling foreach
on a Resource
To incorporate a deployed resource into a foreach
composition without destroying the resource, specify an index as a second argument in the moveTo
function. The index should correspond to the key in the TerraformIterator
named iterator
.
const iterator = TerraformIterator.fromMap({
"website-static-files": {
name: "website-static-files",
tags: { app: "website" },
},
images: { name: "images", tags: { app: "image-converter" } },
});
new S3Bucket(this, "iterator-bucket", {
forEach: iterator,
bucket: iterator.getString("name"),
tags: iterator.getStringMap("tags"),
}).addMoveTarget("resourceWithIterator");
new S3Bucket(this, "moved-bucket", {
bucket: "website-static-files",
tags: { app: "website" },
}).moveTo("resourceWithIterator", "website-static-files");
This allows you to efficiently integrate existing resources into a foreach
composition without destroying the existing deployed resource.
Move By Resource Address
In instances where the move target workflow does not easily fit your use case, resource moves can be performed by directly specifying the full resource address to be moved to/from.
Move To
new S3Bucket(this, "bucket-moved", {
bucket: "move-bucket",
}).moveToId("aws_s3_bucket.bucket-new");
new S3Bucket(this, "bucket-new", {
bucket: "move-bucket",
});
Move From
// Old resource
// new S3Bucket(this, "bucket-moved", {
// bucket: "move-bucket",
// });
new S3Bucket(this, "bucket-new", {
bucket: "move-bucket",
}).moveFromId("aws_s3_bucket.bucket-moved");
Note that in moving from a specified resource address, the original resource being moved from must be removed.
Nested Constructs
Resource addresses in the context of nested constructs will not simply be the specified id given to the resource. When dealing with moving resources by their address to/from nested constructs, run cdktf synth
and refer to cdktf.out/stacks/'your stack name'
to find the exact address to move to/from.
new S3Bucket(this, "bucket-moved", {
bucket: "bucket-moved",
}).moveToId("aws_s3_bucket.nested-construct_bucket-new_5A0C9225"); // moving to S3Bucket "bucket-new" in NestedConstruct "nested-construct"
new NestedConstruct(this, "nested-construct", {});
Moving or renaming modules
To change the id of a module without losing its state use the terraform state mv
command. This command needs to be run in the output directory of the stack that contains the module. Commonly this is cdktf.out/stacks/<stack-name>
. The command takes two arguments, the first is the current id of the module, the second is the new id of the module.
$ cd cdktf.out/stacks/refactoring-example
$ terraform state mv module.my-module module.my-new-module
For further details on the terraform state mv
command refer to the Terraform documentation.
Moving Resources From One Stack To Another
You may want to move a resource from one stack to another, like the following example aws S3Bucket
resource.
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket";
class MyFirstStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", {
region: "us-east-1",
});
// Old Definition
// new S3Bucket(this, "my-bucket", {
// bucketPrefix: "my-bucket",
// });
}
}
class MySecondStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", {
region: "us-east-1",
});
// New Definition
new S3Bucket(this, "my-other-bucket", {
bucketPrefix: "my-bucket",
});
}
}
const app = new App();
new MyFirstStack(app, "refactoring-example-stack-move-1");
new MySecondStack(app, "refactoring-example-stack-move-2");
app.synth();
When you run a cdktf deploy '*'
, the CLI displays the following output.
...
refactoring-example-stack-move-1 # aws_s3_bucket.my-bucket will be destroyed
# (because aws_s3_bucket.my-bucket is not in configuration)
- resource "aws_s3_bucket" "my-bucket" {
...
refactoring-example-stack-move-2 # aws_s3_bucket.my-other-bucket (my-other-bucket) will be created
...
To preserve the state of the resource, you must move it from one stack to another. You can use terraform import
and terraform state rm
to move the resource's state. First we need to find the Terraform name of the Resource in the old stack.
$ cat cdktf.out/stacks/refactoring-example-stack-move-1/cdk.tf.json | jq '.resource.aws_s3_bucket'
{
"my-bucket": {
"//": {
"metadata": {
"path": "refactoring-example-stack-move-1/my-bucket",
"uniqueId": "my-bucket"
}
},
"bucket_prefix": "my-bucket"
}
}
Now we need to find the name of the resource in the new stack to understand where to move the resource to.
$ cdktf synth # To ensure our synthesized stack is up to date
$ cat cdktf.out/stacks/refactoring-example-stack-move-2/cdk.tf.json | jq '.resource.aws_s3_bucket'
{
"my-other-bucket": {
"//": {
"metadata": {
"path": "refactoring-example-stack-move-2/my-other-bucket",
"uniqueId": "my-other-bucket"
}
},
"bucket_prefix": "my-other-bucket"
}
}
We need to find the id of the resource, we can use the terraform state show
command to find it. The id is depenendent on the resource being imported, see the import docs for more details. We need to run these commands in the directory of the stack we are importing the resource from.
$ cd cdktf.out/stacks/refactoring-example-stack-move-1/
$ terraform init
$ terraform state show aws_s3_bucket.my-bucket
# aws_s3_bucket.my-bucket:
resource "aws_s3_bucket" "my-bucket" {
arn = "arn:aws:s3:::my-bucket20221208141249058600000001"
bucket = "my-bucket20221208141249058600000001"
bucket_domain_name = "my-bucket20221208141249058600000001.s3.amazonaws.com"
bucket_prefix = "my-bucket"
bucket_regional_domain_name = "my-bucket20221208141249058600000001.s3.amazonaws.com"
force_destroy = false
hosted_zone_id = "Z3AQBSTGFYJSTF"
id = "my-bucket20221208141249058600000001"
object_lock_enabled = false
region = "us-east-1"
request_payer = "BucketOwner"
tags_all = {}
grant {
id = "554912fda2704333d162d216be50aefb05562e0bf1709997f1d9417cf46087d5"
permissions = [
"FULL_CONTROL",
]
type = "CanonicalUser"
}
versioning {
enabled = false
mfa_delete = false
}
}
Now that we have the id ("my-bucket20221208141249058600000001"
) we can import it. This command needs to be run in the directory of the stack we are importing the resource to.
$ cd ../refactoring-example-stack-move-2
$ terraform import aws_s3_bucket.my-other-bucket my-bucket20221208141249058600000001
We have imported the resource in the new stack, but we still need to remove it from the old stack. This command needs to be run in the directory of the stack we are removing the resource from.
$ cd ../refactoring-example-stack-move-1
$ terraform state rm aws_s3_bucket.my-bucket
We can verify everything worked as expected by running a cdktf deploy '*'
again. We should now see no changes.
$ cd ../../.. # Back to the root of the project
# The next deploy should show no changes
$ cdktf deploy '*'
Why Do I Need To Find The Terraform Name Of The Resource?
If you use Constructs to organize your code, you might have a level between the generated provider constructs and the TerraformStack
construct to organize your code.
This extra level adds a prefix to the name of the resource to ensure uniqueness. Refer to Constructs for more details.