How to use Parameters in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Fri Apr 23 20217 min read

banner

Photo by Danka & Peter

Updated on Fri Apr 23 2021

Parameters are key value pairs that we pass into the CDK stack at deployment time.

Table of Contents #

  1. Defining CDK Parameters
  2. Deploying with CDK Parameters
  3. Parameters are unresolved Tokens in our CDK code
  4. How to use CDK Parameter Values
  5. Caveats when using CDK Parameters
  6. CDK and CloudFormation Parameters

Defining CDK Parameters #

Parameters are key-value pairs that we pass into a CDK stack at deployment time.

Since we pass these key value pairs at deployment time, we aren't able to access the resolved values in our CDK code at synthesis time - i.e. in conditional statements.

It's recommended to define CDK parameters at the stack level. The reason being - parameters derive their name from their logical ID, so if we refactor our code the logical ID could change, which means that the parameter would get deleted and re-created with a new name. The name would be set to the new logical ID. This would be quite confusing.

To define a parameter in CDK we can use the CfnParameter construct.

The code for this article is available on GitHub
lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ parameter of type Number
const databasePort = new cdk.CfnParameter(this, 'databasePort', {
  type: 'Number',
  description: 'The database port to open for ingress connections',
  minValue: 1,
  maxValue: 10000,
  default: 5432,
  allowedValues: ['1000', '3000', '5000', '5432'],
});
console.log('database port ๐Ÿ‘‰', databasePort.valueAsString);

// ๐Ÿ‘‡ parameter of type String
const tableName = new cdk.CfnParameter(this, 'tableName', {
  type: 'String',
  description: 'The name of the Dynamodb table',
});
console.log('tableName ๐Ÿ‘‰ ', tableName.valueAsString);

// ๐Ÿ‘‡ parameter of type CommaDelimitedList
const favoriteRegions = new cdk.CfnParameter(this, 'favoriteRegions', {
  type: 'CommaDelimitedList',
  description: 'An array of regions',
});
console.log('favoriteRegions ๐Ÿ‘‰ ', favoriteRegions.valueAsList);

Note that we aren't explicitly passing a parameterName property, because one doesn't exist, instead the parameter name is inferred from the logical ID of the resource.

We have defined 3 parameters:

  • databasePort - of type Number
  • tableName - of type String
  • favoriteRegions - of type CommaDelimitedList

For reference, the supported Parameter types are:

  • String - i.e. "production"
  • Number - i.e. 42. CloudFormation validates the input as a number, but the parameter in our code is really a string.
  • List<Number> - an array of integer or float numbers - i.e. user input of 42,4.2 would result in ["42","4.2"]
  • CommaDelimitedList - an array of string literals - i.e. user input of "t3.nano,t3.micro,t3.small" would result in ["t3.nano","t3.micro","t3.small"]
  • AWS Specific Parameter types - values corresponding to resources that already exist in your account, i.e. user input of "AWS::EC2::VPC::Id" means the user must pass an existing VPC id as a parameter
  • SSM parameter types - values corresponding to existing parameters from the Parameter Store service. The user must input an existing parameter key and it will be resolved to the corresponding value.

After defining the parameters in our CDK stack, if we try to deploy without providing any parameters, we would get an error of type:

The following CloudFormation Parameters are missing a value: parameterName

deploy without params error

Things to notice:

  • The parameter names correspond to the logical ID of the resources
  • Because we've passed a default value to our databasePort parameter, it's not required

Deploying with CDK Parameters #

In order to deploy a CDK stack with parameters, we have to pass the --parameters flag when issuing the cdk deploy command.

shell
npx cdk deploy my-stack-name    \
    --parameters myFirstParameter=value1 \
    --parameters mySecondParameter=value=2

Note that we have to use the --parameters flag for every parameter we pass into the template.

When deploying multiple stacks with different parameter values we have to prefix the parameter name with the stack name:

shell
npx cdk deploy my-stack-dev my-stack-prod \
    --parameters my-stack-dev:tableName=devTable \
    --parameters my-stack-prod:tableName=prodTable

For our project, the deployment command looks like:

shell
npx cdk deploy my-cdk-stack \
  --parameters databasePort=1000 \
  --parameters tableName=cool-table \
  --parameters favoriteRegions="us-east-1,us-east-2,us-east-3"

Now that we've successfully deployed our CDK application, we can inspect the parameters section in the CloudFormation console:

cloudformation console parameters

Once we have deployed our stack and set the parameter values, we don't have to pass in the parameters we've already set on subsequent deploys, unless we want to change the values.

The parameter values will be persisted by CloudFormation.

This doesn't matter most of the time, because we should have consistent deployment commands put in place, that specify all the necessary stack parameters. Relying on some state that might or might not be what we expect is probably not a good idea.

Parameters are unresolved Tokens in our CDK code #

When we defined our parameters we put a couple of console.log statements in place:

lib/cdk-starter-stack.ts
console.log('database port ๐Ÿ‘‰', databasePort.valueAsString);
console.log('tableName ๐Ÿ‘‰ ', tableName.valueAsString);
console.log('favoriteRegions ๐Ÿ‘‰ ', favoriteRegions.valueAsList);

Let's look at what the output was when we deployed out CDK stack:

parameters unresolved tokens

We can see that the output is Token values. That was the expected behavior, because only after our CDK code has finished running will our CloudFormation stack execute its deployment and resolve the values.

If you're interested to learn more about Tokens, I've written an article What is a Token in AWS CDK.

In short a Token is an encoded value that will be resolved at deployment time by CloudFormation. In order words - not what we want if we intend to use the value in an if statement.

How to use CDK Parameter Values #

Let's define a dynamodb table and set it's tableName property to the tableName Parameter.

lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ parameter of type String
const tableName = new cdk.CfnParameter(this, 'tableName', {
  type: 'String',
  description: 'The name of the Dynamodb table',
});
console.log('tableName ๐Ÿ‘‰ ', tableName.valueAsString);

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

Let's run the deployment command:

shell
npx cdk deploy my-cdk-stack \
  --parameters databasePort=1000 \
  --parameters tableName=cool-table \
  --parameters favoriteRegions="us-east-1,us-east-2,us-east-3"

If we now check our CloudFormation console we can see that our table has been named cool-table, which corresponds to the parameter value we passed:

Table name from param

We were able to set the table name to be equal to the Parameter value we passed. This is the expected behavior.

If we generate a CloudFormation template based on our current CDK app, we would see the plain CloudFormation Parameters section:

template.yaml
Parameters:
  databasePort:
    Type: Number
    Default: 5432
    AllowedValues:
      - "1000"
      - "3000"
      - "5000"
      - "5432"
    Description: The database port to open for ingress connections
    MaxValue: 10000
    MinValue: 1
  tableName:
    Type: String
    Description: The name of the Dynamodb table
  favoriteRegions:
    Type: CommaDelimitedList
    Description: An array of regions

We could also create a lambda function and pass in the parameters as environment variables:

lib/cdk-starter-stack.ts
const myFunction = new NodejsFunction(this, id, {
  // setting environment variables from params ๐Ÿ‘‡
  environment: {
    databasePort: databasePort.valueAsString,
    tableName: tableName.valueAsString,
  },
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'main',
  entry: path.join(__dirname, `/../src/my-function/index.js`),
});

The function's code could be as simple as:

src/my-function/index.js
async function main(event) {
  console.log('databasePort ๐Ÿ‘‰', process.env.databasePort);
  console.log('tableName ๐Ÿ‘‰', process.env.tableName);

  return {body: JSON.stringify({message: 'SUCCESS'}), statusCode: 200};
}

module.exports = {main};

If we invoke the function we are able to access the parameter values:

lambda response

As a side note, I wasn't able to pass the CommaDelimitedList to the function, couldn't figure it out, reading through the support forum comments, I don't think it's possible to pass commas in lambda environment variables, who knew.

Caveats when using CDK Parameters #

  1. If you deploy the CDK stack with an updated parameter value, but don't change your CDK code, the parameter value does not get updated, which is very confusing.

    The output just states: โœ…๏ธ my-stack (no changes) and the parameter value is not updated in CloudFormation, which we can check using the console.

  2. It's important to note that using Parameters in our CDK applications is not recommended from the AWS team, because Parameter values are not resolved during synthesis time in our CDK code, instead they are resolved at deployment time. This means that we aren't able to use parameter values in conditionals in our CDK code.

    Instead the CDK team recommend using environment variables and context, which are resolved at synthesis time and can be used in our CDK code to conditionally provision / update resources.

    This makes a lot of sense, because we don't have to think about which values resolve when and which values we can use in our CDK code.

CDK and CloudFormation Parameters #

Since CDK gets compiled down to CloudFormation we are able to use CloudFormation Parameters in CDK.

You can think of Parameters as key value pairs that we pass into the CDK stack at deployment time.

Since we pass these key value pairs at deployment time, we aren't able to access the resolved values in our CDK code at synthesis time - i.e. in conditional statements.

An example of parameters in a CloudFormation stack looks like:

template.yaml
Parameters:
  # ๐Ÿ‘‡ defining the DatabasePort parameter
  DatabasePort:
    Default: 5432
    Description:
      The database port to open for the ingress connections
    Type: Number
  # ๐Ÿ‘‡ defining the DatabaseName parameter
  DatabaseName:
    Default: tododb
    Description: Database name
    Type: String

In the snippet above, we defined the DatabasePort and DatabaseName parameters, which we can then pass to our CloudFormation stack at deploy time:

shell
aws cloudformation deploy \
  --template-file template.yaml \
  --parameter-overrides DatabaseName=myDatabase \
                        DatabasePort=1234

To complete the flow we can access the Parameters by using the Ref function in our template's Resources and Outputs sections. For example let's pass the DatabaseName as an environment variable to a Lambda:

template.yaml
ListTodosFunction:
  Type: AWS::Lambda::Function
  Properties:
    Environment:
      Variables:
        databaseName: !Ref DatabaseName
If we can, it's best to avoid Parameters. We should use environment variables or context instead, which we can access in our CDK code at synthesis time.

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