Up until recently at my $DAYJOB
I have been using the AWS CDK cli to create Python based CDK Constructs and then use twine
to deploy them to a private pypi repo. This works fairly well but the built in Jest testing with TypeScript is a lot more feature complete. You can then use jsii
to compile the TypeScript into multiple language outputs. This is the same thing the AWS CDK uses to publish their own packages.
So I decided to explore setting up a pipeline to do so and that lead me down a long rabbit-hole to the tool called projen.
What is projen?
projen‘s project description calls it a
A new generation of project generators
This would put it on a similar level as [cookiecutter] or [yeoman]. Both of which can be used as cli tools to bootstrap new projects, generating boilerplate config and code files to get you up and running quickly.
That doesn’t give a whole lot of details, the README further describes it as a way to
Define and maintain complex project configuration through code.
and
JOIN THE #TemplatesAreEvil MOVEMENT!
While I am not sure if templates are actually evil, it is a pretty cool tool. When using the aforementioned project generator tools, you can only run them once usually. That means any additional setup must be done manually and commited. projen has a different approach of doing all configuration via it and enforcing updating those changes. That way it can be run multiple times, ensuring the same output and not needing to manually tweak files.
Using projen
Initially a CDK projen project can be bootstrapped using the cli:
1 | npx projen new awscdk-construct |
Code can then be added into the src/
directory to create your CDK construct.
So far this is pretty typical CDK development, but let’s say you want to bump the CDK version. Typically you would have to go in and edit your package.json
and bump the version of quite a few CDK packages. But projen makes it easier. To do it simply edit a single line in the .projenrc.js
config file.
cdkVersion
1 | const { AwsCdkConstructLibrary } = require('projen'); |
Or add a directory to be ignored in .gitignore. In this case I use a python development environment to work on my lambda. The virtual env directory is within the repo at .venv
.
1 | project.gitignore.addPatterns('.venv/'); |
Then run npx projen
and watch the config files in the repo get updated. Many files managed by projen will let you know via a comment in them. That way it is known not to directly edit it but to instead use projen.
Example in .gitignore
:
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". |
GitHub Actions
The nice thing about the CDK project project is it sets up a build and release pipeline within GitHub Actions with minimal effort. All of this is done via the jsii-release package. To get this working a few things are needed. In my case I am releasing to npm for TypeScript and pypi for Python languages. So I need the following tokens added as secrets to my GitHub Project:
NPM_TOKEN
TWINE_USERNAME
: This will default to__token__
if using pypi.orgTWINE_PASSWORD
Once those are added any PR to the project will result in a build and test be completed via actions. Then once merged into main
a release workflow will run and push those packages to the artifact repositories.
Another added benefit is a API.md
file is generated with references for the Constructs created. Here is an example.
An interesting problem I ran into when using the PythonFunction with GitHub Actions is the construct uses Docker under the hood to install dependencies. This caused issues because Docker was unable to be called within the Action. The solution is to use the L2 Construct SingletonFunction and the local bundle option. This is well described in this AWS blog post.
Here is an example of how it was done in TypeScript CDK with a Python Lambda:
1 | this.lambdaFunction = new lambda.SingletonFunction(this, 'TransformFunction', { |
One other thing to call out here, the use of SingletonFunction
, this way the lambda will be created only once, only if the Construct is actually used multiple times in a given stack.
My first multi-language construct
All of this was discovered when writing and creating my first open source CDK construct: https://github.com/1davidmichael/Cloudwatch-Alarms-to-Chat-Platforms
Does it work? I think so. Is it good? Maybe.
I’ve learned a lot through this about projen and have appreciated what it sets up, its replayability in generating configs, and the automated workflows it sets up.