Organizing AWS CDK with Separated Lambda and Fargate Code in a Monorepo
Oleksandr Hanhaliuk
Posted on October 19, 2024
AWS Cloud Development Kit (CDK) offers a great solution for packaging Lambda functions with NodeJsFunction, which automatically bundles the required packages for your Lambda handler during deployment.
However, a common practice is to place your Lambda handler code in the same repository as your CDK infrastructure code, often sharing the same package.json file. It might not be ideal for organizing large projects, especially when dealing with multiple resources like Lambdas and Fargate containers.
In this article, we’ll explore an alternative approach: separating your business logic (Lambda handler) and infrastructure code (CDK) into different folders, mimicking a monorepo structure.
Step 1: Using Business Code in Separate Folders
Let’s start by separating your Lambda function’s business logic from the CDK infrastructure code. We’ll create two distinct folders: /cdk for CDK code and /lambda for your Lambda handler's code.
1.1 Folder Structure
/cdk
└── cdk-stack.ts
/lambda
└── src
└── handlers
└── index.ts
└── package.json
└── tsconfig.json
1.2 CDK Code for Lambda Deployment
In your CDK stack, define the Lambda using NodejsFunction from the aws-cdk-lib/aws-lambda-nodejs module. In this setup, we'll define the entry point to the Lambda, its handler, and where to find the necessary dependencies like the package-lock.json and tsconfig.json files.
new lambdaNode.NodejsFunction(stack, 'example-service-lambda', {
entry: path.resolve(__dirname, '../lambda/src/handlers/index.ts'), // path to your Lambda handler
handler: 'handler', // the handler function
depsLockFilePath: path.resolve(__dirname, '../lambda/package-lock.json'), // package-lock file
projectRoot: path.resolve(__dirname, '../lambda'), // set project root to the Lambda folder
bundling: {
tsconfig: path.resolve(__dirname, '../lambda/tsconfig.json'), // tsconfig file for TypeScript support
},
});
Now, your business logic is separated from your CDK code, making it easier to manage and scale as needed.
Step 2: Adding Fargate Support
What if you want to deploy more than just Lambda functions, such as Fargate tasks? We can extend this monorepo structure to include Fargate services as well. Let’s create a new folder called /fargate for storing the Fargate-related Dockerfiles and configuration.
2.1 Folder Structure
/cdk
└── cdk-stack.ts
/lambda
└── src
└── handlers
└── index.ts
└── package.json
└── tsconfig.json
/fargate
└── Dockerfile
└── package.json
└── tsconfig.json
2.2 CDK Code for Fargate Task Definition
In your CDK stack, define a Fargate task by pointing it to the Dockerfile inside the /fargate folder.
taskDefinition.addContainer('containerId', {
image: ContainerImage.fromAsset('../fargate', { file: 'Dockerfile' }),
portMappings: [
{
containerPort: 80,
hostPort: 80,
},
],
});
Now you have a Fargate service defined in a separate folder, just like the Lambda service. This structure keeps your infrastructure organized as you add more services.
Step 3: Using Shared Code Between Resources
In many cases, you may have shared logic that you want to use across multiple resources, such as both Lambda functions and Fargate containers. We can organize this shared code into a /shared folder and set up proper imports to access this code in your Lambda or Fargate services.
3.1 Folder Structure
/cdk
└── cdk-stack.ts
/lambda
└── src
└── handlers
└── index.ts
└── package.json
└── tsconfig.json
/fargate
└── Dockerfile
└── package.json
└── tsconfig.json
/shared
└── utils.ts
└── package.json
└── tsconfig.json
3.2 Configuring CDK to Use Shared Code
To access the shared code from your Lambda or Fargate service, modify your /cdk/tsconfig.json to define the path to the shared folder:Copy code
{
"compilerOptions": {
"paths": {
"shared/*": ["../shared/*"]
}
}
}
Now, you can import shared utilities into your Lambda code like this:
import { util } from 'shared/utils';
3.3 Adding Shared Code to the Lambda Bundle
In order to include the shared code in the Lambda bundle, update the NodejsFunction definition to handle the bundling process. We’ll use the volumes and commandHooks options to copy the shared folder into the Lambda container before bundling and clean it up afterward.
new lambdaNode.NodejsFunction(stack, 'example-service-lambda', {
entry: path.resolve(__dirname, '../lambda/src/handlers/index.ts'),
handler: 'handler',
depsLockFilePath: path.resolve(__dirname, '../lambda/package-lock.json'),
projectRoot: path.resolve(__dirname, '../lambda'),
bundling: {
volumes: [
{
// Mount the shared folder to the Lambda bundling container
hostPath: path.resolve(__dirname, '../../../shared'),
containerPath: '/mnt/shared',
},
],
commandHooks: {
beforeBundling(inputDir: string): string[] {
// Copy the shared folder into the Lambda bundling directory
return [`cp -r /mnt/shared ${inputDir}/shared`];
},
afterBundling(inputDir: string): string[] {
// Clean up the shared folder after bundling
return [`rm -rf ${inputDir}/shared`];
},
},
tsconfig: path.resolve(__dirname, '../lambda/tsconfig.json'),
},
});
This ensures that the shared code is included in the Lambda bundle and available at runtime.
Conclusion
By organizing your Lambda, Fargate, and shared code in a monorepo structure, you gain greater flexibility in managing and scaling your AWS infrastructure. Separating your business logic from your CDK code not only keeps your repository clean but also enables easier testing, faster deployments, and better long-term maintainability.
Posted on October 19, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.