CDK Best Practices¶
Code Formatting¶
On the platform engineering team, we have “informally standardized” on using Prettier for formatting our TypeScript files. What is Prettier?
- An opinionated code formatter
- Supports many languages
- Integrates with most editors
- Has few options
Prettier plugins can be installed for IntelliJ or for Visual Studio Code. Once installed in your code editors, you can take advantage of auto-formatting on file save. For VS Code, this is enabled by default. For IntelliJ, you have to manually enable it. To do so, go to Preferences > Languages & Frameworks > JavaScript > Prettier
and click the "On 'Reformat Code' action" and "On save" options. Then click OK. The caveat with the IntelliJ plugin is you have to enable it for each project (at least for version 2022.3.2 (Ultimate Edition)) unless you follow these instructions to configure it on new projects.
The auto formatting is really handy because it handles all of the basic common sense parts of code standards for you. Which means, you'll no longer need to debate stylistic aspects of your code during the review phase.
CDK Organization¶
CDK applications should be organized into logical units, such as API, database, and monitoring resources.
Constructs¶
In CDK, a construct is a basic building block that represents a cloud resource, such as an Amazon S3 bucket, an Amazon EC2 instance, or an Amazon RDS database.
A CDK construct can be thought of as a class definition in object-oriented programming, where each instance of the class represents a unique instance of the cloud resource it represents.
The logical units should be implemented as constructs including the following:
-
Infrastructure (such as Amazon S3 buckets, Amazon RDS databases, or an Amazon VPC network)
-
Runtime code (such as AWS Lambda functions)
-
Configuration code
Constructs should reside in the infrastructure/lib/constructs
directory of your repository. Here are a couple of examples that you can use for reference: CloudWatchAlarmConstruct and DynamoDBConstruct
Stacks¶
AWS CDK Stacks are logical constructs that define a set of AWS resources that are created, managed, and destroyed together as a single unit. They define the deployment model of the logical construct units. A stack in AWS CDK is essentially a blueprint that defines the resources and their properties, dependencies, and relationships within an AWS environment.
Overall, AWS CDK stacks provide an efficient and flexible way to manage AWS infrastructure as code, enabling developers to focus on building and deploying their applications without worrying about the underlying infrastructure.
Stacks should reside in the infrastructure/lib/stacks
directory of your repository. You may refer to BestPracticesStack as an example.
Coding Organization¶
Start simple and add complexity only when you need it¶
The guiding principle for most of our best practices is to keep things simple as possible—but no simpler. Add complexity only when your requirements dictate a more complicated solution. With the AWS CDK, you can refactor your code as necessary to support new requirements. You don't have to architect for all possible scenarios upfront.
Every application starts with a single package in a single repository¶
A single package is the entry point of your AWS CDK app. The app's constructs define the logical units of your solution. Use additional packages for constructs that you use in more than one application.
AWS does not recommend putting multiple applications in the same repository, especially when using automated deployment pipelines. Doing this increases the "blast radius" of changes during deployment. When there are multiple applications in a repository, changes to one application trigger deployment of the others (even if the others haven't changed). Furthermore, a break in one application prevents the other applications from being deployed.
Keep in mind that a construct can be arbitrarily simple or complex. A Bucket
is a construct, but CameraShopWebsite
could be a construct, too.
Infrastructure and runtime code live in the same package¶
In addition to generating AWS CloudFormation templates for deploying infrastructure, the AWS CDK also bundles runtime assets like Lambda functions and Docker images and deploys them alongside your infrastructure. This makes it possible to combine the code that defines your infrastructure and the code that implements your runtime logic into a single construct. It's a best practice to do this. These two kinds of code don't need to live in separate repositories or even in separate packages.
To evolve the two kinds of code together, you can use a self-contained construct that completely describes a piece of functionality, including its infrastructure and logic. With a self-contained construct, you can test the two kinds of code in isolation, share and reuse the code across projects, and version all the code in sync.
Construct Best Practices¶
Constructs are reusable, composable modules that encapsulate resources. Constructs are the basic building blocks of AWS CDK apps. A construct represents a cloud component and encapsulates everything that AWS CloudFormation needs to create the component. A construct can represent a single cloud resource (such as an AWS Lambda function), or it can represent a higher-level component consisting of multiple AWS CDK resources.
AWS CDK includes the AWS Construct Library, which contains constructs representing Amazon Web Services (AWS) resources. This library includes constructs that represent all the resources available on AWS.
For more information, see "Constructs" in the AWS Cloud Development Kit Developer Guide.
Construct parameters¶
Constructs are implemented in classes that extend from the Construct base class. You define a construct by instantiating the class. All constructs take 3 parameters when they are initialized: scope, id and props.
export class ServiceConstruct extends Construct {
constructor(scope: Construct, id: string, props: ServiceStackProps) {
super(scope, id, props)
const vpc = Vpc.fromLookup(this, 'vpc', {
vpcName: props.vpcName,
})
Scope¶
The first argument, scope, is the construct in which this construct is created. In most cases, you define a construct in the scope of itself, which means you usually pass `this for the first argument.
Id¶
The second argument, id, is the local identifier of the construct that must be unique within this scope. AWS CDK uses this identifier to calculate the AWS CloudFormation logical ID for each resource defined within this scope. In this example, MyVpc
is is passed as the ID.
Props¶
The third argument, props, is a set of initialization properties that are specific to each construct and define its initial configuration. For example, the Vpc.Function construct accepts properties such as maxAzs, cidr, and subnetConfiguration. You can explore the various options using the auto-complete feature of your IDE or in the online documentation.
Model with constructs, deploy with stacks¶
Stacks are the unit of deployment: everything in a stack is deployed together. So when building your application's higher-level logical units from multiple AWS resources, represent each logical unit as a Construct
, not as a Stack
. Use stacks only to describe how your constructs should be composed and connected for your various deployment scenarios.
For example, if one of your logical units is a website, the constructs that make it up (such as an Amazon S3 bucket, API Gateway, Lambda functions, or Amazon RDS tables) should be composed into a single high-level construct. Then that construct should be instantiated in one or more stacks for deployment.
By using constructs for building and stacks for deploying, you improve reuse potential of your infrastructure and give yourself more flexibility in how it's deployed.
Configure with properties and methods, not environment variables¶
Environment variable lookups inside constructs and stacks are a common anti-pattern. Both constructs and stacks should accept a properties object to allow for full configurability completely in code. Doing otherwise introduces a dependency on the machine that the code will run on, which creates yet more configuration information that you have to track and manage.
In general, environment variable lookups should be limited to the top level of an AWS CDK app. They should also be used to pass in information that's needed for running in a development environment. For more information, see Environments.
Unit test your infrastructure¶
To consistently run a full suite of unit tests at build time in all environments, avoid network lookups during synthesis and model all your production stages in code. (These best practices are covered later.) If any single commit always results in the same generated template, you can trust the unit tests that you write to confirm that the generated templates look the way you expect. For more information, see Testing constructs.
Don't change the logical ID of stateful resources¶
Changing the logical ID of a resource results in the resource being replaced with a new one at the next deployment. For stateful resources like databases and S3 buckets, or persistent infrastructure like an Amazon VPC, this is seldom what you want. Be careful about any refactoring of your AWS CDK code that could cause the ID to change. Write unit tests that assert that the logical IDs of your stateful resources remain static. The logical ID is derived from the id
you specify when you instantiate the construct, and the construct's position in the construct tree. For more information, see Logical IDs.
Wrapper constructs aren't enough for compliance¶
Writing your own "L2+" constructs might prevent developers from taking advantage of AWS CDK packages such as AWS Solutions Constructs or third-party constructs from Construct Hub. These packages are typically built on standard AWS CDK constructs and won't be able to use your wrapper constructs.
Stack Best Practices¶
Stacks are a unit of deployment in AWS CDK and are made up of constructs and compositions. All AWS resources defined within the scope of a stack, directly or indirectly, are provisioned as a single unit. Because AWS CDK stacks are implemented through AWS CloudFormation stacks, they have the same limitations. You can define any number of stacks in an AWS CDK app.
For more information, see "Stacks" in the AWS Cloud Development Kit Developer Guide.
For more information about AWS CloudFormation limits and restrictions, see limits and restrictions in the AWS CloudFormation FAQs.
Stack Names¶
Stack names need to start with cicd-
or app-
. A Cloudformation ARN with app-*
or cicd-*
in the stack name portion is used in conjunction with the IAM policy to perform CloudFormation actions on stacks with the prefix. The prefix gives the platform team the ability to separate application stacks from infrastructure stacks, so application teams can't break the platform easily.
AWS is not very consistent with their naming conventions so just add those prefixes to any custom stack name. You don't have to stick to an UpperCamelCased
stack name.
Stack Dependencies¶
Two different stack instances can have a dependency on one another. This happens when an resource from one stack is referenced in another stack. In that case, CDK records the cross-stack referencing of resources, automatically produces the right CloudFormation primitives, and adds a dependency between the two stacks. You can also manually add a dependency between two stacks by using the stackA.addDependency(stackB)
method.
For more information, see "stack-dependencies" in the aws-cdk-lib readme.
Tagging¶
CloudFormation supports propagation of stack-level tags for resources with the Tags
property, which should be used to tag your AWS stacks and the resources defined within. Follow this guide for how to tag your AWS stacks via CDK.
Application Best Practices¶
Your CDK application is an app, and is represented by the AWS CDK App class. To provision infrastructure resources, all constructs that represent AWS resources must be defined, directly or indirectly, within the scope of a stack construct. Made up of one or more stacks, Apps can contain one or more constructs. The App construct represents the entire AWS CDK app. This construct is normally the root of the construct tree.
For more information, see “Apps” in the AWS Cloud Development Kit Developer Guide.
Make decisions at synthesis time¶
Try to make all decisions, such as which construct to instantiate, in your AWS CDK application by using your programming language's if
statements and other features. For example, a common CDK idiom, iterating over a list and instantiating a construct with values from each item in the list, simply isn't possible using AWS CloudFormation expressions.
Treat AWS CloudFormation as an implementation detail that the AWS CDK uses for robust cloud deployments, not as a language target. You're not writing AWS CloudFormation templates in TypeScript or Python, you're writing CDK code that happens to use CloudFormation for deployment.
Define removal policies and log retention¶
The AWS CDK attempts to keep you from losing data by defaulting to policies that retain everything you create. For example, the default removal policy on resources that contain data (such as Amazon S3 buckets and database tables) is not to delete the resource when it is removed from the stack. Instead, the resource is orphaned from the stack. Similarly, the CDK's default is to retain all logs forever. In production environments, these defaults can quickly result in the storage of large amounts of data that you don't actually need, and a corresponding AWS bill.
Consider carefully what you want these policies to be for each production resource and specify them accordingly. Use Aspects to validate the removal and logging policies in your stack.
Separate your application into multiple stacks as dictated by deployment requirements¶
There is no hard and fast rule to how many stacks your application needs. You'll usually end up basing the decision on your deployment patterns. Keep in mind the following guidelines:
-
It's typically more straightforward to keep as many resources in the same stack as possible, so keep them together unless you know you want them separated.
-
Consider keeping stateful resources (like databases) in a separate stack from stateless resources. You can then turn on termination protection on the stateful stack. This way, you can freely destroy or create multiple copies of the stateless stack without risk of data loss.
-
Stateful resources are more sensitive to construct renaming—renaming leads to resource replacement. Therefore, don't nest stateful resources inside constructs that are likely to be moved around or renamed (unless the state can be rebuilt if lost, like a cache). This is another good reason to put stateful resources in their own stack.
Commit cdk.context.json
to avoid non-deterministic behavior¶
Determinism is key to successful AWS CDK deployments. An AWS CDK app should have essentially the same result whenever it is deployed to a given environment.
Since your AWS CDK app is written in a general-purpose programming language, it can execute arbitrary code, use arbitrary libraries, and make arbitrary network calls. For example, you could use an AWS SDK to retrieve some information from your AWS account while synthesizing your app. Recognize that doing so will result in additional credential setup requirements, increased latency, and a chance, however small, of failure every time you run cdk synth
.
Never modify your AWS account or resources during synthesis. Synthesizing an app should not have side effects. Changes to your infrastructure should happen only in the deployment phase, after the AWS CloudFormation template has been generated. This way, if there's a problem, AWS CloudFormation can automatically roll back the change. To make changes that can't be easily made within the AWS CDK framework, use custom resources to execute arbitrary code at deployment time.
Even strictly read-only calls are not necessarily safe. Consider what happens if the value returned by a network call changes. What part of your infrastructure will that impact? What will happen to already-deployed resources?
Following are two example situations in which a sudden change in values might cause a problem.
-
If you provision an Amazon VPC to all available Availability Zones in a specified Region, and the number of AZs is two on deployment day, then your IP space gets split in half. If AWS launches a new Availability Zone the next day, the next deployment after that tries to split your IP space into thirds, requiring all subnets to be recreated. This probably won't be possible because your Amazon EC2 instances are still running, and you'll have to clean this up manually.
-
If you query for the latest Amazon Linux machine image and deploy an Amazon EC2 instance, and the next day a new image is released, a subsequent deployment picks up the new AMI and replaces all your instances. This might not be what you expected to happen.
These situations can be pernicious because the AWS-side change might occur after months or years of successful deployments. Suddenly your deployments are failing "for no reason" and you long ago forgot what you did and why.
Fortunately, the AWS CDK includes a mechanism called context providers to record a snapshot of non-deterministic values. This allows future synthesis operations to produce exactly the same template as they did when first deployed. The only changes in the new template are the changes that you made in your code. When you use a construct's .fromLookup()
method, the result of the call is cached in cdk.context.json
. You should commit this to version control along with the rest of your code to make sure that future executions of your CDK app use the same value. The CDK Toolkit includes commands to manage the context cache, so you can refresh specific entries when you need to. For more information, see Runtime context.
If you need some value (from AWS or elsewhere) for which there is no native CDK context provider, we recommend writing a separate script. The script should retrieve the value and write it to a file, then read that file in your CDK app. Run the script only when you want to refresh the stored value, not as part of your regular build process.
Let the AWS CDK manage roles and security groups¶
CDK allows you to define Service Roles, with attached Permission Boundaries, that could be consumed by your CDK app to grant access to other resources in your stack or outside your stack. A Service Role is an AWS Identity and Access Management (IAM) role that allows AWS services to interact with resources in your account on your behalf. A Permission Boundary is an IAM policy that defines the maximum permissions that can be granted to a principal (such as a user, group, or role) in your account.
With the AWS CDK construct library's grant()
convenience methods, you can create AWS Identity and Access Management roles that grant access to one resource by another using minimally scoped permissions. For example, consider a line like the following:
myBucket.grantRead(myLambda)
This single line adds a policy to the Lambda function's role (which is also created for you). That role and its policies are more than a dozen lines of CloudFormation that you don't have to write. The AWS CDK grants only the minimal permissions required for the function to read from the bucket.
We use service control policies and permission boundaries to make sure that developers stay within the guardrails, giving teams flexibility to design their applications.
By attaching a Permission Boundary to a Service Role, you can ensure that any permissions granted to that role are restricted to the permissions defined in the Permission Boundary. This can help you to enforce least privilege and reduce the risk of accidental or malicious access to your resources. Furthermore, by granting permissions to the Service Role rather than individual resources, you can reduce the complexity of your permissions model and make it easier to manage and audit access to your resources.
Model all production stages in code¶
In traditional AWS CloudFormation scenarios, your goal is to produce a single artifact that is parameterized so that it can be deployed to various target environments after applying configuration values specific to those environments. In the CDK, you can, and should, build that configuration into your source code. Create a stack for your production environment, and create a separate stack for each of your other stages. Then, put the configuration values for each stack in the code. Use services like Secrets Manager and Systems Manager Parameter Store for sensitive values that you don't want to check in to source control, using the names or ARNs of those resources.
When you synthesize your application, the cloud assembly created in the cdk.out
folder contains a separate template for each environment. Your entire build is deterministic. There are no out-of-band changes to your application, and any given commit always yields the exact same AWS CloudFormation template and accompanying assets. This makes unit testing much more reliable.