What is a Token in AWS CDK

avatar

Borislav Hadzhiev

Fri Apr 23 20214 min read

banner

Photo by Joshua Earle

Updated on Fri Apr 23 2021

A token in CDK is an encoded value, most commonly an attribute on a resource, that will be resolved at deployment time by CloudFormation

Tokens in AWS CDK #

Some values in our CDK code can't get resolved at synthesis time, but rather get resolved at deployment time. These values are called Tokens.

Tokens are encoded values, most commonly, attributes of resources we define in our CDK stack, that will only get resolved at deployment time.

For example let's define a simple Lambda function and try to access its functionName property in our CDK code:

const myFunction = new NodejsFunction(this, id, {
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'main',
  entry: path.join(__dirname, `/../src/path/index.js`),
});

console.log('function name ๐Ÿ‘‰', myFunction.functionName);

If we run npx cdk deploy we get a result like ${Token[Token.123]}:

function name token

We can see that the value of the functionName attribute is a token at synthesis time.

Explicitly setting Resource names #

Even if we explicitly set the functionName property on the Lambda we wouldn't be able to access the resolved value in our code.

Let's add a dynamodb table to our code and explicitly set the tableName and functionName properties on the resources:

const myTable = new dynamodb.Table(this, 'my-table', {
  // ๐Ÿ‘‡ explicitly setting tableName
  tableName: 'my-table',
  partitionKey: {name: 'todoId', type: dynamodb.AttributeType.NUMBER},
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

console.log('table name ๐Ÿ‘‰', myTable.tableName);

const myFunction = new NodejsFunction(this, id, {
  // ๐Ÿ‘‡ explicitly setting functionName
  functionName: 'my-function',
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'main',
  entry: path.join(__dirname, `/../src/path/index.js`),
});

console.log('function name ๐Ÿ‘‰', myFunction.functionName);

If we run npx cdk deploy we can see that we still get the unresolved token values.

with set names

Obviously, if we hard code the table and function names we have access to these values in our CDK code, but trying to access the attributes still returns a Token.

On a side note you shouldn't explicitly assign names to CDK resources. Check out my other article - Don't assign names to CDK resources.

How CDK Tokens actually work #

Since CDK gets compiled down to CloudFormation code, all these tokens are represented as CloudFormation Ref functions.

At deployment time CloudFormation goes and replaces these references with the actual values - in our case the table and function names.

Let's update our code to remove the explicitly set resource names and pass the dynamodb table name as an environment variable to the lambda and inspect our CloudFormation template:

const myTable = new dynamodb.Table(this, 'my-table', {
  partitionKey: {name: 'todoId', type: dynamodb.AttributeType.NUMBER},
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

const myFunction = new NodejsFunction(this, id, {
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'main',
  entry: path.join(__dirname, `/../src/path/index.js`),
  // ๐Ÿ‘‡ passing in tableName as env variable
  environment: {
    tableName: myTable.tableName,
  },
});

Let's synth our CDK stack and look at the produced cloudformation template:

shell
npx cdk synth --no-staging > template.yaml

The important parts of the CloudFormation template are:

template.yaml
Resources:
  mytable0324D45C:
    Type: AWS::DynamoDB::Table

  mycdkstack4E08F0DD:
    Type: AWS::Lambda::Function
    Properties:
      Environment:
        Variables:
          tableName:
            Ref: mytable0324D45C

Notice how the table name environment variable points to a reference that will eventually get replaced with the real value from CloudFormation.

When we deploy our stack, we can see that the value of the environment variable is resolved and accessible in our Lambda function:

resolved value in lambda

Caveats with CDK Tokens #

A common source of confusion with Tokens in CDK is when we try to access the region, accountId or availabilityZones properties in an environment agnostic stack and get Token values back.

It's a common requirement to conditionally update resource types based on the account or region we are deploying to.

const app = new cdk.App();

new MyCdkStack(app, 'my-cdk-stack', {
  stackName: `my-cdk-stack`,
  // ๐Ÿ‘‡ not specifying the environment
  // env: {
  //   region: process.env.CDK_DEFAULT_REGION,
  //   account: process.env.CDK_DEFAULT_ACCOUNT,
  // },
});

If we now try to access the region, accountId or availabilityZones in our CDK code:

export class MyCdkStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ๐Ÿ‘‡ recommended way to get access to the properties
    console.log('accountId: ', cdk.Stack.of(this).account);
    console.log('region: ', cdk.Stack.of(this).region);
    console.log('availability zones: ', cdk.Stack.of(this).availabilityZones);
  }
}

We would simply get back unresolved Token values:

env agnostic stack props

The way to solve this is to uncomment the commented lines in the snippet above and explicitly set the environment for our stack. I have written an article on this - Get region and accountId in CDK

Exporting Token values as Outputs #

It's a common requirement to export resource attributes as Outputs, so that they can be accessed from our frontend code.

For example to provide our Frontend code access to an API's url we would use Outputs and write the Outputs to a JSON file, which the frontend code can import and use.

Let's see how we can export our lambda and table names and when the values get resolved.

const myTable = new dynamodb.Table(this, 'my-table', {
  partitionKey: {name: 'todoId', type: dynamodb.AttributeType.NUMBER},
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

const myFunction = new NodejsFunction(this, id, {
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'main',
  entry: path.join(__dirname, `/../src/path/index.js`),
  environment: {
    tableName: myTable.tableName,
  },
});

// ๐Ÿ‘‡ export values as Outputs
new cdk.CfnOutput(this, 'tableName', {value: myTable.tableName});
new cdk.CfnOutput(this, 'lambdaName', {value: myFunction.functionName});

Let's deploy our stack and see if the values are solved in the file:

shell
npx cdk deploy --outputs-file ./cdk-exports.json

After deployment, if we now open the cdk-exports.json file the result looks like:

cdk-exports.json
{
  "my-cdk-stack": {
    "tableName": "my-cdk-stack-mytable0324D45C-11VORISY29H3L",
    "lambdaName": "my-cdk-stack-mycdkstack4E08F0DD-KVZpFjrsLC3F"
  }
}

The concept is the same, our CloudFormation template has specified references to these resources, which will eventually be replaced with the real values at deployment time:

template.yaml
Resources:
  mytable0324D45C:
    Type: AWS::DynamoDB::Table
  mycdkstack4E08F0DD:
    Type: AWS::Lambda::Function

# ๐Ÿ‘‡ Outputs reference the resources
Outputs:
  tableName:
    Value:
      Ref: mytable0324D45C
  lambdaName:
    Value:
      Ref: mycdkstack4E08F0DD

Discussion #

Tokens are placeholder values that get replaced with the real value at deploy time by CloudFormation.

If we try to access resource attributes in our CDK code, i.e. in conditional statements, we would get an encoded Token value.

The most common source of confusion is trying to access the accountId, region or availabilityZones properties and getting Token values, because we have not set the environment of the stack explicitly - How to get accountId and region in AWS CDK

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee