CDK applications often end up with environment-specific configurations like database credentials or API endpoints sprinkled throughout the code, making them brittle and hard to manage.
Here’s a CDK app that’s just a bit too eager with its hardcoded values:
# app.py
from aws_cdk import App, Stack, Environment
from constructs import Construct
import os
class MyConfigStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Uh oh, hardcoded values for dev and prod
env_name = os.environ.get("CDK_ENV", "dev") # Default to dev if not set
if env_name == "dev":
db_host = "dev-db.example.com"
api_url = "https://api.dev.example.com"
instance_size = "t3.micro"
elif env_name == "prod":
db_host = "prod-db.example.com"
api_url = "https://api.prod.example.com"
instance_size = "m5.large"
else:
raise ValueError(f"Unknown environment: {env_name}")
# Imagine these are used to create resources
print(f"Config for {env_name}: DB Host={db_host}, API URL={api_url}, Instance Size={instance_size}")
app = App()
# We'll define environments later, but for now, let's see the issue
MyConfigStack(app, "MyConfigStack")
app.synth()
If you run this with export CDK_ENV=prod and then cdk synth, you’ll see the production values printed. Run it with export CDK_ENV=dev and you get dev values. It works, but it’s coupled tightly to the os.environ lookup and the if/elif logic directly within the stack definition.
The core problem is that the definition of your infrastructure (the Stack classes) is intermingled with the values that change per deployment environment. This makes it hard to:
- Reuse Stacks: You can’t easily deploy the same
MyConfigStackto multiple environments without duplicating code or relying on environment variables that are checked during synthesis. - Test Configurations: Verifying that the correct configuration is applied for a given environment requires running
cdk synthwith the right environment variable set, which isn’t ideal for automated testing. - Manage Secrets: Sensitive values are often hardcoded or derived from environment variables, which isn’t secure.
The CDK provides a more robust mechanism for managing environments and their associated configurations directly within the cdk.json and app.py structure.
Using CDK Environments and Context
The CDK’s Environment object is designed to capture details about a deployment target: account, region, and crucially, parameters which can hold arbitrary key-value pairs. You can define these environments in your cdk.json and then access them within your app.py.
1. Define Environments in cdk.json
This is the central place to declare your different deployment targets.
{
"app": "npx ts-node --prefer-ts-exts src/main.ts", // Or your Python equivalent
"watchImage": {
"enabled": true,
"modules": [
"node_modules",
"cdk.out"
]
},
"context": {
// Global context values can go here
},
"//": "Our environment definitions start here",
"env": {
"dev": {
"account": "111122223333",
"region": "us-east-1",
"parameters": {
"dbHost": "dev-db.example.com",
"apiUrl": "https://api.dev.example.com",
"instanceSize": "t3.micro",
"environmentName": "dev"
}
},
"prod": {
"account": "444455556666",
"region": "us-west-2",
"parameters": {
"dbHost": "prod-db.example.com",
"apiUrl": "https://api.prod.example.com",
"instanceSize": "m5.large",
"environmentName": "prod"
}
}
}
}
2. Reference Environments in app.py
Now, in your app.py, you’ll instantiate your stacks with specific environments.
# app.py
from aws_cdk import App, Stack, Environment
from constructs import Construct
import os
class MyConfigStack(Stack):
def __init__(self, scope: Construct, construct_id: str, *, env_config: dict, **kwargs) -> None:
# The 'env_config' dictionary will contain the parameters from cdk.json
super().__init__(scope, construct_id, env=Environment(account=env_config.get('account'), region=env_config.get('region')), **kwargs)
db_host = env_config.get('dbHost')
api_url = env_config.get('apiUrl')
instance_size = env_config.get('instanceSize')
environment_name = env_config.get('environmentName')
# Use these values to configure your resources
print(f"Config for {environment_name} (Account: {self.account}, Region: {self.region}): DB Host={db_host}, API URL={api_url}, Instance Size={instance_size}")
app = App()
# Iterate over the environments defined in cdk.json
# The 'env' key in cdk.json is automatically loaded into app.node.get_context('env')
cdk_envs = app.node.get_context('env')
if not cdk_envs:
raise RuntimeError("No environments defined in cdk.json or context.")
for env_name, env_details in cdk_envs.items():
# Pass the entire environment details dictionary, which includes parameters
MyConfigStack(app, f"MyConfigStack-{env_name}", env_config=env_details)
app.synth()
3. Deploying
When you deploy, you tell CDK which environment to use via the --context flag.
- To deploy to dev:
cdk deploy --context env=dev - To deploy to prod:
cdk deploy --context env=prod
When cdk deploy --context env=dev is executed, CDK:
- Looks up
cdk.json. - Finds the
envsection. - Extracts the configuration for
devbased on the--context env=devflag. - Passes this configuration (including the
parameters) to yourMyConfigStackduring synthesis. - The
MyConfigStackthen uses these values to instantiate its resources.
The env object in cdk.json is a special key that CDK automatically loads into the context. By passing --context env=dev, you’re essentially telling CDK to use the dev configuration block from the env section.
The env_config dictionary you pass to MyConfigStack contains both the account and region (which are also accessible via self.account and self.region on the Stack object), and your custom parameters. This is how you decouple environment-specific values from your stack’s core logic.
This approach centralizes your environment configurations, makes them explicit, and allows you to pass them directly into your stacks without relying on external environment variables or complex conditional logic within the stack definition itself. It’s the idiomatic CDK way to handle environment-specific deployments.
The next thing you’ll want to tackle is managing secrets for these environments, as the current configuration still exposes sensitive values in cdk.json.