DEV Community

loading...
Cover image for Correctly defining CDK dependencies in L3 constructs

Correctly defining CDK dependencies in L3 constructs

udondan profile image Daniel Schroeder ・4 min read

When creating L3 constructs for the AWS CDK, the easiest thing to get wrong is defining dependencies. And with wrong dependency definitions you make it hard to impossible to use your package. This post will show how you correctly define CDK dependencies.

As a CDK user, you already know, all CDK core packages have to be of the same version or you will get cryptic errors such as:

unable to determine cloud assembly output directory. Assets must be defined indirectly within a "Stage" or an "App" scope
Enter fullscreen mode Exit fullscreen mode

So what you want to avoid when publishing L3 constructs, is to directly depend your package on any version of the core CDK packages. Still, this is the case in almost every L3 construct I have seen. I'm assuming this is because the core packages themselves do it like this and developers learn from reading code.

Usually you find packages having either exact or caret versions in their dependencies. Also, in most cases, these definitions are accompanied with the same items in the peerDependencies. Let's analyze these two setups and what the result for the end-user is going to be:

Dependencies with caret version

{
  ...
  "dependencies": {
    "@aws-cdk/aws-lambda": "^1.23.0"
  },
  "peerDependencies": {
    "@aws-cdk/aws-lambda": "^1.23.0"
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

The caret definition means, install the latest minor compatible to the given version. That is everything < 2.0.0. So until CDK 2 lands, this will install the very latest package for aws-lambda.

The end-user now can only install your package, when all his dependencies refer to the latest CDK packages as well. If he uses CDK 1.70.0 there is no way he can install your package without causing incompatibility between core packages. The solution from user perspective is to upgrade the CDK and live with the potentially introduced breaking changes.

Dependencies with exact version

{
  ...
  "dependencies": {
    "@aws-cdk/aws-lambda": "1.80.0"
  },
  "peerDependencies": {
    "@aws-cdk/aws-lambda": "1.80.0"
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

A package with this dependencies definition of course is only compatible with exactly one version of the CDK. This means your users cannot upgrade the CDK without upgrading your package and vice versa. To make your package usable by future CDK versions you need to release new versions of your package. Most probably you do this automated. And either you have a mapping between CDK version and your package version in your docs or you use the same exact version as the upstream packages, which renders the information (e.g. semver) of your version string useless. How do you progress your own code? How do you communicate bug-fixes or breaking changes? And let's not forget, you waste computational resources for compiling, transferring and storing your build artifacts without actual change.

The problem though, is not the format of your dependency definition (exact vs. caret) - the problem simply is: You have dependencies.

And as simple as that problem statement, is the solution: Just don't.

Thou shall not list any CDK core package in your dependencies. Instead, list them in the peerDependencies and devDependencies. You should use caret versions, defining the minimum version required by your package. If you don't know or don't care, use ^1.0.0.

{
  ...
  "devDependencies": {
    "@aws-cdk/aws-lambda": "^1.0.0"
  },
  "peerDependencies": {
    "@aws-cdk/aws-lambda": "^1.0.0"
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

You need them in the devDependencies to build your package with jsii and you need them in the peerDependencies to let the user know what packages need to be installed along with your package.

Now, when a user installs your package, what happens depends on the used language and package manager. Either way, the user needs to define the peer-dependencies as dependencies of the application, e.g. in the package.json or requirements.txt.

npm version < 6 will warn about missing peer dependencies:

npm WARN cdk-awesome-package@1.2.3 requires a peer of @aws-cdk/aws-lambda@^1.0.0 but none was installed.
Enter fullscreen mode Exit fullscreen mode

The same goes for yarn:

warning " > cdk-awesome-package@1.2.3" has unmet peer dependency "@aws-cdk/aws-lambda@^1.0.0".
Enter fullscreen mode Exit fullscreen mode

In npm 6 for some reason this is missing and the user will only know about the missing dependency when he runs the application. That's fine though, the error message is clear about what package needs to be installed. An entry in the package.json needs to be made:

{
  ...
  "dependencies": {
    "@aws-cdk/aws-lambda": "1.70.0",
    "cdk-awesome-package": "^1.2.3"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

All other languages supported by jsii don't support peer dependencies. For these languages they are converted to normal dependencies. pip correctly interprets the caret version definition of ^1.0.0 and in return you would install the latest version. For the dotnet package, jsii converted ^1.0.0 to 1.0.0... which in the end really doesn't matter. Because all this can and has to be overridden by the user anyway.

With pip the user can override the version of dependencies by simply adding them to his own requirements.txt or setup.py, e.g.:

aws-cdk-aws-lambda==1.70.0
cdk-awesome-package>=1.2.3
Enter fullscreen mode Exit fullscreen mode

In C# the user can also override the version in the project file, e.g.

  <ItemGroup>
    <PackageReference Include="Amazon.CDK.AWS.LAMBDA" Version="1.70.0" />
    <PackageReference Include="CDK.Awesome.Package" Version="1.*" />
  </ItemGroup>
Enter fullscreen mode Exit fullscreen mode

I haven't checked Java but I assume the same can be archived in a gradle file.

And there you go. A CDK L3 construct working with any version of core CDK packages.

Discussion

pic
Editor guide