Using Deployment Time Values from Parameter Store in a Launch Template's User-Data Script with AWS CDK
I’ve been starting to port all of our hand-managed infrastructure over to AWS CDK. CDK is a program by Amazon available for a few programming languages which allows you to define your infrastructure in code. I for myself use it with Python. CDK comes with two different types of classes: L1 and L2 constructs. L1 constructs are a direct mapping between Cloudformation types and CDK classes, L2 constructs are an abstraction. I use only L1 constructs, because I feel I have better control with them.
The AWS Parameter Store allows you to store configuration variables in basically a key-value store. They advertise it as a way to hierarchically store configuration, but in my opinion it’s only a key-value store with a prefix search. E.g. it’s lacking functionality to propagate values from higher levels into deeper levels of the configuration, but that’s another topic. I’m using it to store our configuration, that’s what we need to know for this blog post.
Now, there has been one thing I’ve struggled with for a while. When you combine CDK and AWS Parameter Store there are two ways you can retrieve the parameters. You can either resolve them at synthesis time, i.e. when CDK creates the Cloudformation template as a YAML file. Or you can resolve them at deployment time, in which case CDK will write the load commands into the YAML and Cloudformation will read from the Parameter Store when you deploy. All variables that are resolved at synthesis time are stored into the CDK context and thus are not resolved again when you deploy an update. Instead the values are read from the context.
One thing I am storing in the Parameter Store is the version of the product
each our our customers has. I am using this information in a user-data script
of a launch template to git checkout
the right tag of the product before I
run it. I wanted to load this value during deployment time, so that it does not
get stored into the CDK context. Otherwise I would have had to execute
cdk context --reset
each time I wanted to deploy a new version.
With Cloudformation directly it’s simple to use expressions in a user-data
script: Just combine the strings with Fn::Join
, e.g. like this:
"MyLaunchTemplate": {
"Type": "AWS::EC2::LaunchTemplate",
"Properties": {
"LaunchTemplateData": {
"ImageId": "ami-xxx",
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"#!/bin/bash -xe",
{"Fn::Sub": "echo \"${SomeParameter}\""},
]
}
}
}
}
}
}
However, when I wanted to pass a dictionary with this structure to the
userData
field of a CDK CfnLaunchTemplate
it always complained that it
cannot handle a dictionary. It worked fine with a base64-encoded string.
However, base64 only allowed me to use values that are already resolved at synthesis
time, because to encode a value with base64 in Python I have to know
its actual value in Python - i.e. at synthesis time.
# reads a value to be resolved at synthesis time
product_version = ssm.StringParameter.value_from_lookup(
self, '/Prod/MyProduct/Deploy/Version')
launch_template = ec2.CfnLaunchTemplate(self, 'My-LT', launch_template_data={
'imageId': my_ami,
'userData': base64.b64encode(
f'echo {product_version}'.encode('utf-8')).decode('utf-8'),
})
A few weeks later I finally found the solution and when you know it, it’s
extremely simple: The Cloudformation functions are available as Python
functions in aws_cdk.core.Fn
!
So with the following code it’s possible to use deployment time variables in a user-data script of a launch template in CDK:
import aws_cdk.core as cdk
# loads a value to be resolved at deployment time
product_version = ssm.StringParameter.value_for_string_parameter(
self, '/Prod/MyProduct/Deploy/Version')
launch_template = ec2.CfnLaunchTemplate(self, 'My-LT', launch_template_data={
'imageId': my_ami,
'userData': cdk.Fn.base64(cdk.Fn.join(' ', ['echo', product_version])),
})