Introduction 👋
Often projects will have different configuration values for each deployed environment. This could be feature toggles, URLs for third-party services or any number of other variables.
With the AWS CDK, this is simple to configure. I have seen a few different approaches to this problem. In this blog I'll share a few suitable for use in TypeScript.
Method 1️⃣: Stack Configuration Function
This approach uses a mapper function to return the configuration, you can see we have a single configuration property defined in the EnvironmentConfig
interface.
interface IEnvironmentConfig {
readonly myEnvSpecificApiEndpoint: string;
}
const environmentConfig = (environment: string): IEnvironmentConfig => {
const environmentMapper: {
[environment: string]: {
myEnvSpecificApiEndpoint: string;
};
} = {
local: {
myEnvSpecificApiEndpoint: 'https://dev.google.com/api',
},
test: {
myEnvSpecificApiEndpoint: 'https://test.google.com/api',
},
production: {
myEnvSpecificApiEndpoint: 'https://google.com/api',
},
};
return environmentMapper[environmentName];
};
This function would be called from the stack like this, process.env.ENV_NAME
would correspond to the environment name (replace this with your environment name variable for your chosen CI/CD pipeline) or default to local if undefined.
const environment: string = process.env.ENV_NAME || 'local';
const envConfig: IEnvironmentConfig = environmentConfig(environment);
You can then access the configuration like this:
const apiEndpoint: string = envConfig.myEnvSpecificApiEndpoint;
The apiEndpoint
variable is now ready to be used in your stack.
Method 2️⃣: CDK Runtime Context
CDK Context values are key-value pairs that can be associated with a stack or construct. There are a number of different ways these values can be configured, for more information on that see the link above to the documentation.
In this example, I'm going to use cdk.context.json
file in the root of a CDK project to configure a stack.
Here is an example cdk.context.json
:
{
"local":{
"myEnvSpecificApiEndpoint":"https://dev.google.com/api"
},
"test":{
"myEnvSpecificApiEndpoint":"https://test.google.com/api"
},
"production":{
"myEnvSpecificApiEndpoint":"https://google.com/api"
}
}
Interfaces can also be created to define the type of configuration data expected:
interface IEnvironmentConfig {
readonly myEnvSpecificApiEndpoint: string;
}
These values can be accessed like this:
const environment: string = process.env.ENV_NAME || 'local';
const envConfig: IEnvironmentConfig = scope.node.tryGetContext(environment);
const apiEndpoint: string = envConfig.myEnvSpecificApiEndpoint;
Method 3️⃣: Extending StackProps
The AWS CDK StackProps interface can be extended to add additional configuration properties. In this example we will extend the AWS CDK interface to add a property called myEnvSpecificApiEndpoint
.
interface IEnvironmentConfig extends StackProps {
readonly myEnvSpecificApiEndpoint: string;
}
Now in the stack initialisation file (located in the bin
directory) we can pass this in.
const app = new cdk.App();
new TheScheduledLambdaStack(app, 'TheScheduledLambdaStack',{
myEnvSpecificApiEndpoint: 'https://dev.google.com/api'
});
Now, the one downfall of this is that you still need to implement something like method 1 or 2 to configure it on a per-environment basis. This would look something like this for method 1:
new TheScheduledLambdaStack(app, 'TheScheduledLambdaStack',{
myEnvSpecificApiEndpoint: envConfig.myEnvSpecificApiEndpoint;
});
Conclusion 🤌
Looking at all three methods, I personally like method 2. Until recently I was blissfully unaware the CDK had already 'solved' this problem for us.
I live by the saying 'code is a liability' - the less code you manage the better.
Do you..
- use any of these methods already?
- have a better way of doing it?
- have a different opinion on the optimal solution?
Let me know in the comments!
Top comments (4)
I prefer to use method 3 for Constructs, and to some extent Stacks.
Per-environment data may come from configuration parameters you store in your project, from Systems Manager Parameter Store, Secrets Manager, other stacks, etc.
At least for Constructs I try to keep the interface the same, so it does not need to care which way the parameter data has been retrieved, thus always method 3 for those.
For Stacks, that may vary a bit.
Thank you for taking the time to put this together.
I have been struggling with this for weeks, and even AWS Support does not know how to do it.
I have two environments, dev and prod. I have two cli profiles configured, dev and prod.
I did not set a default config, so I force myself to always declare the profile like this $cdk deploy stackName --profile dev
What I am trying to do is set property values based on the profile being used to deploy.
if ---> $cdk deploy stackName --profile prod
then ---> someProperty : productionDomain.com
if --->$cdk deploy stackName --profile dev
then ---> someProperty : developmentDomain.com
I read (and re-read) your post but it's still over my head. Please let me know if you would be willing to dumb it down to my example above.
It might be to late to answer this, I also new in using CDK but I implement such functionality by providing additional context through cdk command option
--context
and then retrieve the rest of the context defined incdk.json
file based on it, as the sample shown below,cdk.json
file:bin/app.ts
file:I synth the cdk app by issuing command
cdk synth --profile prod --context env=dev
and theconf
constant result would be like,Also I want to keep the
env
context provided receivesdev
orprod
value only, hence I implement schema validation using zod while parsing theenv
contextI do this:
github.com/jamesearlywine/document...