toJsonString() resolves tokens and then JSON.stringifys the result. When a token's rendered Terraform expression contains a string literal, the quotes inside ${…} get escaped, and Terraform rejects the attribute. This breaks aws_ecs_task_definition.container_definitions whenever a container field references something like Fn.lookup(map, "key").
Environment: terraconstructs 0.2.18 · cdktn 0.24.0 · hashicorp/aws 6.61.0 · Terraform 1.10.5. Reproduced on the current ECS code (task-definition.ts has had no changes to this rendering since v0.2.18).
Reproduction
import { ecsTaskDefinition } from "@cdktn/provider-aws";
import { Fn, TerraformVariable, Testing } from "cdktn";
const stack = new AwsStack(Testing.app(), "TestStack", { /* ... */ });
const arns = new TerraformVariable(stack, "secret_arns", { type: "map(string)" });
const td = new ecs.Ec2TaskDefinition(stack, "TaskDefinition", {
networkMode: ecs.NetworkMode.BRIDGE,
});
td.addContainer("main", {
image: ecs.ContainerImage.fromRegistry("nginx:latest"),
memoryLimitMiB: 512,
environment: { SECRET_ARN: Fn.lookup(arns.value, "my-secret") },
});
Rendered container_definitions value (what Terraform evaluates as a template):
... "value":"${var.secret_arns[\"my-secret\"]}" ...
as written to the .tf.json file:
"${var.secret_arns[\\\"my-secret\\\"]}"
terraform validate:
Error: Invalid character
on cdk.tf.json line …, in resource.aws_ecs_task_definition.…:
This character is not used within the language.
Error: Invalid expression
Expected the start of an expression, but found an invalid expression token.
Expressions without string literals are unaffected, e.g. ${data.aws_partition.p.dns_suffix} or ${data.aws_secretsmanager_secret.s.name}. We hit it with a Secrets Manager valueFrom of Fn.lookup(remoteState.outputs.secret_arns, "…"). The same expression rendered through a hand-written JSON.stringify of an L1 containerDefinitions string validated fine, so this surfaced as a regression when moving from L1 to the ECS L2.
Mechanism
src/aws/compute/ecs/base/task-definition.ts (main, ~L712–715):
containerDefinitions: Lazy.stringValue({
produce: () => this.stack.toJsonString(this.renderContainers()),
}),
toJsonString() (src/stack-base.ts, ~L364–376) is Tokenization.resolve(...) followed by JSON.stringify(...). By the time JSON.stringify runs, the token is already the literal text ${var.secret_arns["my-secret"]}. So its inner quotes get escaped as JSON string content, but they sit inside a Terraform template interpolation, where a backslash is not valid.
Workaround
Hoist the expression into a TerraformLocal. The container definition then references ${local.<name>}, which has no quotes to escape, and the local carries the original expression:
const secretArn = new TerraformLocal(stack, "secret_arn", Fn.lookup(arns.value, "my-secret"));
td.addContainer("main", {
// ...
environment: { SECRET_ARN: secretArn.asString },
});
Rendered: "value":"${local.secret_arn}". terraform validate passes, and the resolved value is unchanged.
Other call sites with the same pattern
toJsonString() is shared, so these likely share the bug class. I have only reproduced the ECS task definition case:
src/aws/compute/batch/ecs-job-definition.ts (containerProperties)
src/aws/compute/batch/multinode-job-definition.ts
src/aws/notify/rule.ts (eventPattern)
src/aws/notify/input.ts
src/aws/cloudwatch/dashboard.ts (dashboard body)
src/aws/compute/state-machine.ts (definition)
src/aws/storage/ecr-repository.ts (repository policy)
Possible direction
Let Terraform do the encoding instead of JavaScript, e.g. render the attribute as Fn.jsonencode(resolvedObject) so string escaping happens after interpolation. The alternative is to make toJsonString() aware of ${…} spans and leave their contents unescaped. Either way, a synth-level test that runs the output through terraform validate (or asserts that no \" appears inside ${…}) would catch regressions across all of the call sites above.
Related but different: #116 (Lazy-produced nested blocks bypassing camelCase→snake_case conversion) is the same general family of Lazy output skipping normal cdktn rendering, but a different defect.
toJsonString()resolves tokens and thenJSON.stringifys the result. When a token's rendered Terraform expression contains a string literal, the quotes inside${…}get escaped, and Terraform rejects the attribute. This breaksaws_ecs_task_definition.container_definitionswhenever a container field references something likeFn.lookup(map, "key").Environment: terraconstructs 0.2.18 · cdktn 0.24.0 · hashicorp/aws 6.61.0 · Terraform 1.10.5. Reproduced on the current ECS code (
task-definition.tshas had no changes to this rendering since v0.2.18).Reproduction
Rendered
container_definitionsvalue (what Terraform evaluates as a template):as written to the
.tf.jsonfile:terraform validate:Expressions without string literals are unaffected, e.g.
${data.aws_partition.p.dns_suffix}or${data.aws_secretsmanager_secret.s.name}. We hit it with a Secrets ManagervalueFromofFn.lookup(remoteState.outputs.secret_arns, "…"). The same expression rendered through a hand-writtenJSON.stringifyof an L1containerDefinitionsstring validated fine, so this surfaced as a regression when moving from L1 to the ECS L2.Mechanism
src/aws/compute/ecs/base/task-definition.ts(main, ~L712–715):toJsonString()(src/stack-base.ts, ~L364–376) isTokenization.resolve(...)followed byJSON.stringify(...). By the timeJSON.stringifyruns, the token is already the literal text${var.secret_arns["my-secret"]}. So its inner quotes get escaped as JSON string content, but they sit inside a Terraform template interpolation, where a backslash is not valid.Workaround
Hoist the expression into a
TerraformLocal. The container definition then references${local.<name>}, which has no quotes to escape, and the local carries the original expression:Rendered:
"value":"${local.secret_arn}".terraform validatepasses, and the resolved value is unchanged.Other call sites with the same pattern
toJsonString()is shared, so these likely share the bug class. I have only reproduced the ECS task definition case:src/aws/compute/batch/ecs-job-definition.ts(containerProperties)src/aws/compute/batch/multinode-job-definition.tssrc/aws/notify/rule.ts(eventPattern)src/aws/notify/input.tssrc/aws/cloudwatch/dashboard.ts(dashboard body)src/aws/compute/state-machine.ts(definition)src/aws/storage/ecr-repository.ts(repository policy)Possible direction
Let Terraform do the encoding instead of JavaScript, e.g. render the attribute as
Fn.jsonencode(resolvedObject)so string escaping happens after interpolation. The alternative is to maketoJsonString()aware of${…}spans and leave their contents unescaped. Either way, a synth-level test that runs the output throughterraform validate(or asserts that no\"appears inside${…}) would catch regressions across all of the call sites above.Related but different: #116 (Lazy-produced nested blocks bypassing camelCase→snake_case conversion) is the same general family of Lazy output skipping normal cdktn rendering, but a different defect.