Create Construct Library - Construct #
Next, we will create a construct library project leveraging Projen to synthesize and manage project configuration files. Then, as an Internal Construct Hub Producer, we’ll create a construct called Hitcounter
. Lastly, we’ll edit the configuration to transpile our constructs to the selected runtime/language targets.
Set Up a Projen Project #
From the construct-lib-repo
directory, Create a directory named constructs/
. It should be a sibling (on the same level) of the pipeline/
directory.
mkdir constructs
cd constructs
Run the following command to scaffold an awscdk-construct type Projen project
npx projen new awscdk-construct \
--build-workflow false \
--github false \
--default-release-branch main \
--no-git \
--name "cdkworkshop-lib"
The .projenrc.js
file holds the Projen configuration.
Open the file .projenrc.js
and make the following two changes.
-
Import the ReleaseTrigger class from projen’s library.
const { ReleaseTrigger } = require('projen/lib/release');
-
After the
repositoryUrl
attribute add the following attributes listed below:
description: 'CDK Construct Library by projen/jsii',
python: {
distName: 'hitcounter',
module: 'cdkworkshop-lib',
},
dotnet: {
dotNetNamespace: 'CDKWorkshopLib',
packageId: 'com.cdkworkshop.HitCounter',
},
publishToMaven: {
javaPackage: 'com.cdkworkshop.hitcounter',
mavenArtifactId: 'constructs',
mavenGroupId: 'cdkworkshop-lib',
},
majorVersion: 1,
releaseTrigger: ReleaseTrigger.manual(),
The file should look something like this:
const { awscdk } = require('projen');
const { ReleaseTrigger } = require('projen/lib/release');
const project = new awscdk.AwsCdkConstructLibrary({
author: 'CDK Workshop',
authorAddress: 'cdkworkshop@amazon.com',
buildWorkflow: false,
cdkVersion: '[Insert Latest CDK Version]', //Make sure to update this value to the latest version
defaultReleaseBranch: 'main',
github: false,
name: 'cdkworkshop-lib',
releaseTagPrefix: 'cdkworkshop-lib',
repositoryUrl: 'codecommit::us-east-1://construct-lib-repo',
description: 'CDK Construct Library by projen/jsii',
python: {
distName: 'hitcounter',
module: 'cdkworkshop-lib',
},
dotnet: {
dotNetNamespace: 'CDKWorkshopLib',
packageId: 'com.cdkworkshop.HitCounter',
},
publishToMaven: {
javaPackage: 'com.cdkworkshop.hitcounter',
mavenArtifactId: 'constructs',
mavenGroupId: 'cdkworkshop-lib',
},
releaseTrigger: ReleaseTrigger.manual(),
majorVersion: 1,
});
project.synth();
Make sure to replace the value of cdkVersion
with the latest version (Version 2), which you can find here: CDK Version
The python
, dotnet
and publishToMaven
attributes tell Projen to transpile the CDK Construct to those target runtimes.
The majorVersion
attribute is set to 1
, so we start with version 1.0.0
of packaged artifacts.
The releaseTrigger
attribute is set to manual
. For every commit/push to the repository the pipeline later would do projen release
which would automatically update the published artifacts version number. Projen uses SEMVER and Conventional Commits to figure out which part of the version to increment, for details refer Projen release documentation.
Projen Synth #
Run projen
from the constructs
directory. This will make projen synthesize the configuration. Projen synthesizes project configuration files such as package.json, tsconfig.json, .gitignore, GitHub Workflows, eslint, jest, etc. from a well-typed definition written in JavaScript:
npx projen
Create Lambda Directory #
Create a directory lambda
in the root of the constructs directory (next to src
and test
):
mkdir lambda
Hit Counter Lambda Handler #
We’ll be reusing the Hit counter lambda handler from the TypeScript workshop. Create the file lambda/hitcounter.js
:
const { DynamoDB, Lambda } = require('aws-sdk');
exports.handler = async function(event) {
console.log("request:", JSON.stringify(event, undefined, 2));
// create AWS SDK clients
const dynamo = new DynamoDB();
const lambda = new Lambda();
// update dynamo entry for "path" with hits++
await dynamo.updateItem({
TableName: process.env.HITS_TABLE_NAME,
Key: { path: { S: event.path } },
UpdateExpression: 'ADD hits :incr',
ExpressionAttributeValues: { ':incr': { N: '1' } }
}).promise();
// call downstream function and capture response
const resp = await lambda.invoke({
FunctionName: process.env.DOWNSTREAM_FUNCTION_NAME,
Payload: JSON.stringify(event)
}).promise();
console.log('downstream response:', JSON.stringify(resp, undefined, 2));
// return response back to upstream caller
return JSON.parse(resp.Payload);
};
Add Hit Counter Construct #
Create a new file hitcounter.ts
in the constructs/src
directory. Use the following code:
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface HitCounterProps {
/** the function for which we want to count url hits **/
readonly downstream: lambda.IFunction;
/** the path to the hitcounter lambda directory. Doing it this way allows us to specify the path in the stack itself **/
readonly hitcounterPath: string;
}
export class HitCounter extends Construct {
/*_ allows accessing the counter function */
public readonly handler: lambda.Function;
/*_ the hit counter table */
public readonly table: dynamodb.Table;
constructor(scope: Construct, id: string, props: HitCounterProps) {
super(scope, id);
const table = new dynamodb.Table(this, 'Hits', {
partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING },
});
this.table = table;
this.handler = new lambda.Function(this, 'HitCounterHandler', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'hitcounter.handler',
code: lambda.Code.fromAsset(props.hitcounterPath),
environment: {
DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
HITS_TABLE_NAME: table.tableName,
},
});
// grant the lambda role read/write permissions to our table
table.grantReadWriteData(this.handler);
// grant the lambda role invoke permissions to the downstream function
props.downstream.grantInvoke(this.handler);
}
}
This is very similar to the Hitcounter construct from the TypeScript workshop with some slight modifications.
Next, update index.ts
in constructs/src
by adding the following code on line 1:
export * from './hitcounter';
Note: Projen only transpiles Typescript files in src
folder
Finally, lets add a simple test for our new construct to ensure the projen build process succeeds. Rename the hello.test.ts
file found in constructs\test
directory to hitcounter.test.ts
and replace the contents with the following code:
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { HitCounter } from '../src/hitcounter';
test('DynamoDB Table Created', () => {
const stack = new cdk.Stack();
// WHEN
new HitCounter(stack, 'MyTestConstruct', {
hitcounterPath: 'lambda',
downstream: new lambda.Function(stack, 'TestFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`exports.handler = async function(event) {
console.log("event: ", event);
};
`),
}),
});
// THEN
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::DynamoDB::Table', 1);
});
Projen Tamper Detection #
Projen is opinionated and mandates that all project configuration be done through the .projenrc.js
file. For instance if you directly change package.json
then Projen will detect that during the release phase and will fail the release attempt. Hence, it is a good idea to do projen synth by running the projen
command on the constructs/
directory where the .projenrc.js
file is before pushing the code to our CodeCommit repository.
# This is to avoid Projen tamper related errors
npx projen
Push Initial Commit to CodeCommit Repository #
Ensure you push from the construct-lib-repo
directory:
git add .
git commit -m 'Initial Commit'
git push
Extra Credit (Optional) #
How Do I Generate JSSI Transpiled Packages Locally? #
The compile
command below needs to be run on constructs
folder where .projenrc.js
file is. It will compile the typescript files in src/
folder and place it on to lib/
folder.
npx projen compile
The package
command below will transpile to the target language and place it on the dist/
directory.
npx projen package:js
Inspect dist/js/
directory contents to see the generated artifact.
Summary #
In this section, we structured the construct library construct code in a way that is expected by the construct library pipline code. In the next section we deploy the pipeline to build, transpile, and publish artifacts to our Internal Construct Hub.