Update 2/7/2022: Read Synth CDK app to Custom Bucket instead.
Consulting requires you to work within the client’s parameters. Some clients have internal standards and want you to deliver your white-label CDK app as CloudFormation. Call me old fashioned but…
I dont expect Apple to rewrite their products in TypeScript because that is my current favorite.
Joking aside, in consulting this is a pretty common ask, so you must be prepared to deal with it.
On a related note, some AWS Clients have compliance or security restrictions that make it very difficult to get CLI access to deploy using CDK.
Fortunately, you can synth CDK apps into plain old CloudFormation, package it to an S3 bucket, and deploy it from the CloudFormation web console.
TL;DR;
Check out the demo project on GitHub.
Initial Product Stack
Our demo stack will create a bucket, and name it based on the account, region, and client name.
1 | interface MyProductProps extends StackProps { |
The env Field
Typically you will pass an env
field to your CDK stack props like this:
1 | const app = new cdk.App(); |
And it’s very easy to access account
and region
from props like this:
1 | const bucketName = `${props.env.account}-${props.env.region}-${props.client}`; |
The synthesized template will look like this:
1 | BucketName: '123456789123-us-east-1-foo' |
This is very intuitive and works great for CDK deploys, but IS NOT PORTABLE. The synthesized template will contain hard-code account and region values.
The above props are evaluated at synth-time
. We want the account and region values to be evaluated at deploy-time
.
Introducing Stack.of()
In plain CloudFormation you wouldn’t hard-code the account and region information, you would use pseudo-functions like: !Ref AWS::AccountId
and !Ref AWS::Region
which get evaluated at deploy-time.
Let’s rewrite our stack params and exclude the optional env
field.
Also, let’s rewrite our stack to use Stack.of when env is not available.
1 | export class MyProduct extends Stack { |
If you look at the synthesized CloudFormation template, the result should look familiar:
1 | BucketName: !Sub '${AWS::AccountId}-${AWS::Region}-foo' |
Bonus: The above approach works whether env
is passed in or not, so we should use it by default.
CloudFormation Parameters and Tokens
The other major requirement for portable apps is CloudFormation Parameters. So far, we have passed in our client
name as a string. This is very convenient for CDK deploys so we want to keep this format, but let’s rewrite our stack to use CloudFormation parameters so that we can have a deploy-time parameter.
1 | export class MyProduct extends Stack { |
We just extended our product stack to make it portable, with just a slight modification in the base stack! If you were to synthesize MyPortableStack
, your bucketName
would look something like this:
1 | BucketName: !Sub '${AWS::AccountId}-${AWS::Region}-${Client}' |
Warning!
valueAsString
generates a token. Tokens don’t represent your data, so don’t perform string manipulations or conditionals with tokens. I will cover how to deal with this in another post.
Synthesizing a Template
So far we have been making our CDK app portable, but we want CDK to generate a plain CloudFormation template.
Create a new bin/product.ts
file
1 | const app = new cdk.App(); |
Create a new synth script in package.json
. Notice this script excludes metadata and version reporting, this is not required for plain CloudFormation and makes our output template a lot cleaner.
1 | { |
Run the script npm run synth:product
Your cdk.out/template.yaml
should look like this:
1 | Parameters: |
CloudFormation Logical Ids
If you are upgrading an existing CloudFormation or SAM app to CDK, you will notice that the bucket logical ID (Bucket83908E77
) is auto-generated.
If our legacy template logical ID was Bucket
, this would force the bucket to be recreated if you updated the stack.
We must update the build
step to use overrideLogicalId
to specify our logical ID.
1 | const bucket = new Bucket(this, 'Bucket', { |
Now the template has our expected logical ID, and we can update our stack without losing our data.
1 | Parameters: |
Conclusion
This is a lot of extra work if you are building internal AWS apps, but if you are upgrading or building a product that will be installed in client accounts, you need the flexibility to support different deployment mechanisms.
Check out the GitHub Repo for full project setup.