CDK Tokens are special objects that represent a value that will only be known at deployment time, not during synthesis.

Let’s see this in action. Imagine you have a Lambda function that needs to know the ARN of a DynamoDB table. You can’t hardcode the ARN because the table doesn’t exist until CloudFormation creates it during deployment. This is where tokens come in.

from aws_cdk import core as cdk
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_dynamodb as dynamodb

class MyStack(cdk.Stack):
    def __init__(self, scope: cdk.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # 1. Create the DynamoDB table
        table = dynamodb.Table(self, "MyTable",
            partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING),
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST
        )

        # 2. Create the Lambda function
        my_lambda = lambda_.Function(self, "MyLambda",
            runtime=lambda_.Runtime.PYTHON_3_9,
            handler="index.handler",
            code=lambda_.Code.from_asset("lambda"),
            environment={
                "TABLE_ARN": table.table_arn  # table.table_arn is a Token!
            }
        )

        # 3. Grant Lambda permissions to read/write to the table
        table.grant_read_write_data(my_lambda)

In this example, table.table_arn is a CDK Token. When you synthesize this CDK app (cdk synth), the CDK knows how to get the ARN (it knows it’s a dynamodb.Table resource with a table_arn attribute), but it doesn’t know the actual string value. The CDK’s CloudFormation generator will insert a special CloudFormation intrinsic function (like !Ref MyTable) into the generated template. Later, when CloudFormation deploys this stack, it will resolve !Ref MyTable to the actual ARN of the created DynamoDB table.

The problem arises when you try to use a token in a way that requires its resolved value during synthesis. For instance, if you tried to construct a string that must contain the final ARN at synthesis time, like:

# This will cause a "Cannot resolve token" error during synthesis
invalid_url = f"https://{table.table_arn}/data"

Here, the f-string is evaluated during cdk synth. Since table.table_arn is a token (its value isn’t known until deployment), Python can’t interpolate it into the string, leading to an error.

To handle situations where you need a value that is only known at deployment time but must be used in a string or other construct that requires a concrete value during synthesis, you use Lazy Values.

Lazy values are created using cdk.Lazy or cdk.Fn.join (which implicitly creates lazy values for its arguments if they are tokens). A lazy value accepts a function (a "thunk") that will be executed during deployment to produce the final value.

Here’s how you’d correctly construct that URL using a lazy value:

from aws_cdk import core as cdk
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_dynamodb as dynamodb

class MyStack(cdk.Stack):
    def __init__(self, scope: cdk.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        table = dynamodb.Table(self, "MyTable",
            partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING),
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST
        )

        # Using cdk.Fn.join to construct the URL lazily
        # cdk.Fn.join will ensure its arguments are resolved at deployment time
        # if they are tokens.
        valid_url = cdk.Fn.join("", [
            "https://",
            table.table_arn, # This is a token
            "/data"
        ])

        my_lambda = lambda_.Function(self, "MyLambda",
            runtime=lambda_.Runtime.PYTHON_3_9,
            handler="index.handler",
            code=lambda_.Code.from_asset("lambda"),
            environment={
                "TABLE_ARN": table.table_arn,
                "DATA_URL": valid_url # valid_url is now a lazy value
            }
        )

        table.grant_read_write_data(my_lambda)

In this corrected version, cdk.Fn.join is used. When the CDK synthesizes this, it doesn’t try to resolve table.table_arn into a string. Instead, it generates CloudFormation that instructs CloudFormation to construct the string https://<resolved_table_arn>/data at deployment time. The environment variable DATA_URL will receive this dynamically constructed URL.

The underlying mechanism is that CDK tokens are implemented as subclasses of core.Token. When a token is encountered in a context where a string is expected, the CDK doesn’t try to evaluate it immediately. Instead, it checks if the context allows for a token (like an environment variable for a Lambda function, or an attribute for another resource) or if it needs to be resolved. If it needs to be resolved into a string at synthesis time and cannot be, you get the "Cannot resolve token" error. cdk.Lazy and cdk.Fn functions are the tools to defer this resolution until deployment.

The most surprising thing about CDK tokens is that they aren’t just placeholders for strings; they are objects that carry metadata about how to obtain their value at deployment time. This metadata is what allows CDK to generate the correct CloudFormation.

Consider the cdk.Fn.get_att function. This is another way to create a token that retrieves an attribute from a CloudFormation resource.

# Example: Getting the ARN of a different resource
other_resource = cdk.CfnResource(self, "OtherResource",
    type="AWS::SomeService::SomeResource",
    properties={
        "SomeProperty": "someValue"
    }
)

# This is a token that will resolve to the ARN of 'OtherResource' at deployment
other_resource_arn_token = other_resource.get_att("Arn").to_string()

# You can use this token in environment variables or other constructs
my_lambda_2 = lambda_.Function(self, "MyLambda2",
    runtime=lambda_.Runtime.PYTHON_3_9,
    handler="index.handler",
    code=lambda_.Code.from_asset("lambda"),
    environment={
        "OTHER_RESOURCE_ARN": other_resource_arn_token
    }
)

Here, other_resource.get_att("Arn") returns a token. .to_string() is called on this token. This doesn’t immediately resolve the ARN. Instead, it tells the CDK that the string representation of this attribute should be obtained at deployment time. The generated CloudFormation will use Fn::GetAtt for this.

The reason cdk.Fn.join is so powerful is that it can accept a list of strings and tokens. It then generates a CloudFormation Fn::Join intrinsic function, ensuring that all token arguments within the list are resolved at deployment time before the join operation occurs. This is essential for constructing complex strings that depend on deployed resources.

The most common mistake is trying to perform string interpolation or concatenation directly with a token during cdk synth, as shown in the initial invalid example. Always use cdk.Fn.join or cdk.Lazy when you need to build a string that includes values only known at deployment time.

The next thing you’ll likely encounter is needing to pass specific parts of a resource’s attributes, not just the whole ARN, and how to reference those using Fn::GetAtt and then use them within other CloudFormation resources.

Want structured learning?

Take the full Cdk course →