Deploying your CDK app to different stages and environments

by Thorsten Höger, Cloud Automation Evangelist

CDK recap

The AWS Cloud Development Kit (CDK) helps you define your Cloud infrastructure using a higher-level programming language that is then synthesized into AWS CloudFormation to create your infrastructure in the AWS environment. You can learn more about CDK from the community at https://cdk.dev

One of the big differences to classic CloudFormation templates is that the top-most object is the CDK app that contains multiple stacks versus on top-level stack in CloudFormation. Given this design, you can define all your stacks your application need in one CDK app.

Why do we need stages?

For every application, you would want to deploy not only one instance for production but also some pre-production setups for testing, QA, load testing, etc. These different sets of infrastructure components are called stages. Each of these stages is deployed into one or more AWS environments, defined by an AWS account id and a region.

We want these stages to be similar to each other, but there will always be differences in some settings like size and number of machines, software versions, 3rd party URLs, etc. So we need to design a way to make all these settings configurable.

Sample application

For this blog post, I want to show this based on a simple sample application that runs an nginx Webserver using ECS and Fargate and has an ApplicationLoadBalancer as an entry point. Using the ECS patterns library of the CDK, this is only one construct named ApplicationLoadBalancedFargateService and has a few settings like container image, number of containers and potentially fixed domain names.

class WebserverStack extends cdk.Stack {
	
	constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
		super(scope, id, props);

		new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
			memoryLimitMiB: 1024,
		  cpu: 512,
		  taskImageOptions: {
		    image: ecs.ContainerImage.fromRegistry('nginx:latest'),
		  },
			desiredCount: 1,
		});
	}
}

const app = new cdk.App();
new WebserverStack(app, 'MyWebserver');

Extracting configuration

The first step is to generalize this code for all our environments by introducing a configuration object that extends the CDK stack properties with our image version and container count settings. We can then use this object to configure our Fargate setup.

interface WebserverStackProps extends cdk.StackProps {
	/**
	 * container image tag
	 * @default latest
	 */
	readonly imageTag?: string;
	/**
	 * number of containers
	 */
	readonly containerCount: number;
}

class WebserverStack extends cdk.Stack {
	
	constructor(scope: cdk.Construct, id: string, props: WebserverStackProps) {
		super(scope, id, props);

		new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
			memoryLimitMiB: 1024,
		  cpu: 512,
		  taskImageOptions: {
		    image: ecs.ContainerImage.fromRegistry(`nginx:${props.imageTag ?? 'latest'}`),
		  },
			desiredCount: props.containerCount,
		});
	}
}

How does CDK handle environments?

In the AWS CDK, every stack has a property called env that defines this stack's target environment. This property receives the account id and the region for this stack. If you do not want to configure the real target data here, you can use the environment variables 'CDK_DEFAULT_ACCOUNT' and 'CDK_DEFAULT_REGION' to let AWS CDK use the account and region of your current AWS CLI credentials. I would not recommend doing this, as it poses the risk of deploying your application to whatever account you are currently logged in instead of a defined target environment. So our app definition should look more like this:

const app = new cdk.App();
new WebserverStack(app, 'MyWebserver', {
	env: {account: '111111111111', region: 'eu-central-1'},
});

Given this target definition and our WebserverStackProps interface, we can now define all the stages we want to deploy within our app.

We define the settings we want, like different version tags or the number of containers for every stage.

const app = new cdk.App();
// Development stage
new WebserverStack(app, 'MyWebserver-dev', {
	env: {account: '111111111111', region: 'eu-central-1'},
	imageTag: '1.19.6',
	containerCount: 1,
});

// QA stage
new WebserverStack(app, 'MyWebserver-qa', {
	env: {account: '111111111111', region: 'eu-central-1'},
	imageTag: '1.19.5',
	containerCount: 2,
});

// Production stage with two environments
new WebserverStack(app, 'MyWebserver-prod-frankfurt', {
	env: {account: '222222222222', region: 'eu-central-1'},
	imageTag: '1.19.3',
	containerCount: 4,
});
new WebserverStack(app, 'MyWebserver-prod-dublin', {
	env: {account: '222222222222', region: 'eu-west-1'},
	imageTag: '1.19.3',
	containerCount: 4,
});

Synthesizing and deploying the app

The AWS CDK deployment has several stages. Your application is synthesized into CloudFormation templates, and these are then finally deployed to the target accounts and regions. The synth step of this pipeline will always create the CloudFormation templates for all stages and environments, which is meant to be this way. The reasoning is that you synthesize once at the beginning of your pipeline and during the execution, which may take days or weeks, you only work with the so-called cloud assembly in the cdk.out folder.

So the workflow for deploying our stacks would be:

# Synthesize all templates to cdk.out
cdk synth

# Deploy our testing stage and reference the assembly
cdk deploy --app 'cdk.out/' MyWebserver-dev

# Do some tests here and approve test stage

# Deploy our qa stage
cdk deploy --app 'cdk.out/' MyWebserver-qa

# Do some tests here and approve QA stage

# Deploy our production stages and reference the assembly
cdk deploy --app 'cdk.out/' MyWebserver-prod-frankfurt
cdk deploy --app 'cdk.out/' MyWebserver-prod-dublin

Conclusion

I strongly recommend to define all your stages for your workload within the same CDK app and configure the differences using custom stack properties. You should also deploy all stages from the same branch and pipeline execution by synthesizing once and using the cloud assembly to run the same artifacts and with the same settings in all stages.

What's next?

To reduce the complexity of writing pipelines, the AWS CDK brings a package called CDK pipelines, which helps you creating deployment workflows using AWS CodePipeline. I recommend you read my post to learn more about this.

Need help?

If you need help setting up multi-stage apps in AWS CDK, reach out to me via my AWS CDK Coaching program for individuals or teams.

More articles

Shift Left Security: Reviewing AWS CDK Apps at Pull Request Time

Shifting security reviews of AWS CDK applications to the pull request phase enables teams to catch potential security issues early by analyzing CloudFormation template diffs before changes reach production. By implementing automated tooling and clear review processes, teams can identify risky IAM permissions, network configurations, and encryption settings while maintaining development velocity. This approach not only reduces security risks but also empowers developers with immediate feedback on their infrastructure changes.

Read more

Three strategies for deploying container images to Amazon ECS using CDK

This blog post explores three strategies for deploying container images to Amazon ECS using CDK: pre-built images, custom images from separate projects, and images built alongside CDK code, providing developers with insights to choose the best approach for their specific needs and workflows.

Read more

Tell me about your project

Taimos GmbH