DEV Community

Cover image for AWS CDK: Per-Environment Configuration Patterns
Gerald Stewart for AWS Community Builders

Posted on

AWS CDK: Per-Environment Configuration Patterns

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];
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

You can then access the configuration like this:

const apiEndpoint: string = envConfig.myEnvSpecificApiEndpoint;
Enter fullscreen mode Exit fullscreen mode

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"
   }
}
Enter fullscreen mode Exit fullscreen mode

Interfaces can also be created to define the type of configuration data expected:

interface IEnvironmentConfig {
  readonly myEnvSpecificApiEndpoint: string;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (3)

Collapse
eriklz profile image
Erik Lundevall Zara

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.

Collapse
daveharig profile image
Dave Harig

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.

Collapse
varvay profile image
Varid Vaya Yusuf • Edited on

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 in cdk.json file based on it, as the sample shown below,
cdk.json file:

{
  ...
  "context": {
    ...
    ],
    "dev": {
      "someProperty": "developmentDomain.com"
    },
    "prod": {
      "someProperty": "productionDomain.com"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

bin/app.ts file:

const app = new App();
const env = app.node.tryGetContext('env');
const conf = app.node.tryGetContext(env);
Enter fullscreen mode Exit fullscreen mode

I synth the cdk app by issuing command cdk synth --profile prod --context env=dev and the conf constant result would be like,

{
   "someProperty": "developmentDomain.com"
}
Enter fullscreen mode Exit fullscreen mode

Also I want to keep the env context provided receives dev or prod value only, hence I implement schema validation using zod while parsing the env context