Converting a CDK construct to using Projen
I've built a few CDK constructs over the last year and found the setup could be a bit laborious. I even created a new project to add as a template for JSII-based constructs. However, this template suffers from all the pains of typical templates, you can't update them after the fact.
Enter projen. The tl;dr: it manages your package.json, .gitignore, and many other project files that you normally have to maintain yourself. I won't cover the basics, just go read the README for Projen, it's thick and juicy.
I'm going to cover here what it took to convert an existing JSII-based CDK construct, nightynight.
The Existing Code
First, a quick review of the existing code:
The code follows a very typical AWS CDK construct/app format. There is a bin
folder which contains a CDK App
.
The lib
folder contains a sample Stack
that allows for some integration testing. The construct itself is in
the nightynight.ts
file and is pretty simple. It creates a scheduled event and a Lambda Function that stops the given instance.
The Lambda Function uses a NodejsFunction which will come into play later.
Getting Started
I made sure I had a clean git repo and had the tool installed. Since I ran Projen a lot, I used the recommended alias:
alias pj="npx projen"
. After this I used pj
to bootstrap a new file: pj new awscdk-construct
. Right away I get
some interesting results:
ā nightynight git:(feat/blogsandbox) pj new awscdk-construct npx: installed 64 in 2.835s š¤ Created .projenrc.js for AwsCdkConstructLibrary š¤ Synthesizing project... š¤ aws-cdk: removed š¤ ts-node: removed š¤ source-map-support: removed š¤ @aws-cdk/aws-ec2: removed š¤ @aws-cdk/aws-events: removed š¤ @aws-cdk/aws-events-targets: removed š¤ @aws-cdk/aws-iam: removed š¤ @aws-cdk/aws-lambda: removed š¤ @aws-cdk/aws-lambda-nodejs: removed š¤ @aws-cdk/core: removed š¤ cdk-iam-floyd: removed š¤ @aws-cdk/aws-ec2: removed š¤ @aws-cdk/aws-events: removed š¤ @aws-cdk/aws-events-targets: removed š¤ @aws-cdk/aws-iam: removed š¤ @aws-cdk/aws-lambda: removed š¤ @aws-cdk/aws-lambda-nodejs: removed š¤ @aws-cdk/core: removed š¤ cdk-iam-floyd: removed Command failed: yarn install --check-files /bin/sh: yarn: command not found
I didn't have yarn installed so that was a simple fix. The removals of all the modules is interesting, and was the first thing I'd address. I kept an eye on my Gitkraken window and a diff of the whole repo. This guided me on what changes I needed to make. I was aiming at having only a few minor modifications to the existing files and additions. Here's how it looked at the start:
This is the .projenrc.js file it generated:
const { AwsCdkConstructLibrary } = require('projen'); const project = new AwsCdkConstructLibrary({ authorAddress: "matthew.bonig@gmail.com", authorName: "Matthew Bonig", cdkVersion: "1.60.0", name: "nightynight", repository: "git@github.com:mbonig/nightynight.git" }); project.synth();
Right away I can see fields like name
, description
and others that simply need setting in the initial props. After a
lot of pj
commands and reviewing diffs I came to this:
let dependencies = { "cdk-iam-floyd": "0.54.1" }; const project = new AwsCdkConstructLibrary({ name: "@matthewbonig/nightynight", description: "A CDK construct that will automatically stop a running EC2 instance at a given time.", authorAddress: "matthew.bonig@gmail.com", authorName: "Matthew Bonig", cdkVersion: "1.60.0", repository: "https://github.com/mbonig/nightynight", bin: { "nightynight": "bin/nightynight.js" }, dependencies: dependencies, peerDependencies: dependencies, devDependencies: { "yarn": "1.22.10", "parcel": "v2.0.0-beta.1" }, cdkDependencies: [ "@aws-cdk/aws-ec2", "@aws-cdk/aws-events", "@aws-cdk/aws-events-targets", "@aws-cdk/aws-iam", "@aws-cdk/aws-lambda", "@aws-cdk/aws-lambda-nodejs", "@aws-cdk/core" ], keywords: [ "cdk", "ec2" ], python: { module: "mbonig.nightynight", distName: "mbonig.nightynight" }, dependabot: false, buildWorkflow: false, releaseWorkflow: false });
I wasn't able to get it to line up exactly with my previous package.json, but it was awfully close. Notice a few specific things:
- The
repository
was set by Projen to the git's remote, which was the ssh address of the repository. However, since I publish this construct to pypi I had to set that back to the https url. - I install
yarn
andparcel
as devDependencies, so that the existing Github Action work without editing. Yarn is used in thescripts
commands and parcel is used by the NodejsFunction. By installing parcel the Github Action doesn't require the use of Docker. - I've turned off the three Github Actions with the
dependabot
,buildWorkflow
andreleaseWorkflow
fields. I'd like to turn those back on but the existing Github Action I have is working well, so they're slightly redundant right now.
However, this wasn't enough. There are some additional changes to the project:
project.addFields({ main: "lib/nightynight.js", types: "lib/nightynight.d.ts", awscdkio: { twitter: "mattbonig" }, public: true });
There are some fields that I need to set directly. Projen provides that release valve through the addFields
method,
since this project type doesn't expose everything available in an NPM project (and it shouldn't). Since my package has
a scope (@matthewbonig) I require the public
field for npmjs to accept it. I've also setup the awscdkio field for the
Construct Catalog.
Next up, I add some specific excludes and includes to git and npm's ignore files:
const tempDirectories = ["cdk.context.json", ".cdk.staging/", ".idea/", ".parcel-cache/", "cdk.out/"]; project.gitignore.exclude(...tempDirectories); project.npmignore.exclude(...tempDirectories);
The options are all just temp directories or files we don't ever want to commit or package up.
Testing
So, feeling pretty comfortable I started testing. I had run pj
a lot, but I hadn't actually tried testing my existing
code, so a simple yarn test
and I was off:
ā nightynight git:(feat/blogsandbox) ā yarn test yarn run v1.22.10 $ rm -fr lib/ && jest --passWithNoTests && yarn eslint FAIL test/nightynight.test.ts ā Test suite failed to run test/nightynight.test.ts:3:29 - error TS2307: Cannot find module '../lib/nightynight' or its corresponding type declarations. 3 import { NightyNight } from '../lib/nightynight'; ~~~~~~~~~~~~~~~~~~~~ Test Suites: 1 failed, 1 total Tests: 0 total Snapshots: 0 total Time: 2.879 s Ran all test suites. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
What? Where'd my construct go?!
The first step of the test
script was to remove the lib
folder, which is where my source
code exists. It expects all source code in src
, so I restored the files using Gitkraken and then
moved them over to the src
directory (updating the reference in the test accordingly). I also cleaned up
the index.ts
file that Projen creates.
At this point I started dealing with a lot of weird errors around testing my construct. Turns out I needed to delete the
jest.config.js
file that was still around.
After some trial and error with publishing I discovered I needed to modify the compile
script:
project.addScript( "compile", "jsii --silence-warnings=reserved-word --no-fix-peer-dependencies && jsii-docgen && cp src/nightynight.handler.ts lib/nightynight.handler.ts" );
All I did was copy the existing compile
script from the package.json file and add the cp
of the handler at the end.
This file can't be compiled and packaged the same as the rest of the construct and needs to be shipped along with the
construct so that
the consumer of this package can compile it. This is specifically because I'm using the NodejsFunction and wouldn't be
needed in many cases. Without this step I was having a problem where a synth with the published construct resulted in an
error about the missing entry file.
I did some tests, committed my code and then watched the Github Action to see it get built and published. There were a few things I found during this trial and error but it generally went pretty smoothly.
Wrap Up
So there it is. About an hour's worth of work and I was all set. I have
the WakeyWakey
construct to convert next. However, now that there is some similarity here I don't want to just copy/paste this code
to that repository. I'll refactor this work into a reusable class that inherits from AwsCdkConstructLibrary! Once again
we see static text files being replaced by reusable classes and a synth
process. If this feels familiar, it should,
considering who created Projen.
Future
Now in the future I expect only a few minor changes regarding Projen. I will want to keep deps on the CDK and iam-cdk-floyd up to date. I probably can't rely on dependabot to do this so I'll have to work something up myself.