Scaling Up: Hot-Reloading Multiple TypeScript Lambdas in LocalStack
Tags: cdk, localstack, hotreload
Published: 2024-01-26 12:51:10

In a recent consultancy project, I was tasked with creating a mock environment for an asynchronous webservice using LocalStack, with the primary aim of minimizing the feedback loop associated with integrating our systems. This involved using `AWS CDK` to provision multiple TypeScript runtime lambdas in LocalStack and `esbuild` to build the functions.

On completing the mock environment, I  decided to go the extra mile by attempting to enable hot reloading for the lambdas, a feature that proved more challenging than expected, as outlined in the official LocalStack [guide](https://docs.localstack.cloud/user-guide/tools/lambda-tools/hot-reloading/#hot-reloading-for-typescript-lambdas).

After navigating through moments of frustration, fueled by a few cups of Yorkshire tea, I eventually uncovered the solution to hot-reloading multiple TypeScript lambdas in LocalStack. Below, I have outlined guide on how to achieve this for others navigating similar challenges.

Before diving into this guide, it's recommended to familiarize yourself with the official hot-reloading Localstack [guide](https://docs.localstack.cloud/user-guide/tools/lambda-tools/hot-reloading/#hot-reloading-for-typescript-lambdas) for a single Lambda function.

The following steps provide a summary of the official guide for hot-reloading TypeScript Lambda functions in LocalStack:

1. **The Build Step**:
 `package.json` ```json
"scripts": {
            "build": "esbuild index.ts \
                       --bundle --minify --sourcemap \
                       --platform=node --target=es2020 \
                       --outfile=dist/index.js --watch"
        },
```Executing the above build script with `npm run build` uses esbuild to bundle and minify the TypeScript code into a single JavaScript file, stored in the `dist` folder.
    
2. **Deploying to LocalStack**  
```bash
awslocal lambda create-function \
   --function-name hello-world \
   --runtime "nodejs16.x" \
   --role arn:aws:iam::123456789012:role/lambda-ex \
   --code S3Bucket="hot-reload",S3Key="$(PWD)/dist" \
   --handler index.handler
```

If followed correctly, these steps should enable hot-reloading for a single Lambda function after every code change.
    

## Hot-Reloading Multiple Lambdas

To extend hot-reloading to multiple Lambdas, a key modification is needed at the build step:

```json
"scripts": {
        "build": "esbuild lambdas/src/*.ts 
                    --bundle --minify --sourcemap \
                    --platform=node --target=es2020 \
                    --outdir=lambdas/build --watch"
    },
```
This command individually transpiles, bundles, and minifies each Lambda entry file in the `lambdas/src` folder, placing the output in the `lambdas/build` directory.

Note: The `lambdas/src` folder should match the directory where your Lambdas reside. Other bundlers, not just esbuild, can be used for the build step.

Proceed to deploy each Lambda to Localstack using the `awslocal lambda create-function` command, and they should hot-reload with every code change.

**Important**: When deploying from within a container, ensure that the `S3key` is a path on the host, as Localstack maps directories from the host to the Lambda containers.

------------------------------------------------------------------------------

## Bonus Information: AWS Cloud Development Kit (CDK) Configuration for Hot-Reloaded Lambda.

```typescript
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Architecture, Code, Function, Runtime } from "aws-cdk-lib/aws-lambda";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";
import { join, basename } from "path";


const STAGE = process.env.STAGE ?? "local";
const LAMBDA_MOUNT_CWD = join(__dirname, "lambdas/build")

function ApplicationFunction(
  scope: Construct,
  id: string,
  props: NodejsFunctionProps
) {
  if (STAGE === "local") {
    return LocalFunction(scope, id, props);
  }
  return new NodejsFunction(scope, id, props);
}

function LocalFunction(
  scope: Construct,
  id: string,
  props: NodejsFunctionProps
) {
  const hotReloadBucket = Bucket.fromBucketName(
    scope,
    `HotReloadingBucket-${id}`,
    "hot-reload"
  );

  if (!props.entry) throw new Error('Entry point is required');

  const fileName = basename(props.entry, ".ts");
  const handler = props.handler ?? "handler";
  const runtime = props.runtime || Runtime.NODEJS_18_X;

  return new Function(scope, id, {
    ...props,
    code: Code.fromBucket(hotReloadBucket, join(__dirname, "lambdas/build")),
    runtime,
    handler: `${fileName}.${handler}`,
  });
}

```

Bootstrap and deploy to LocalStack by running:
```bash
cdklocal bootstrap && cdklocal deploy --require-approval never
```

For a complete working example of hot-reloading multiple TypeScript Lambdas, refer to this example [CRUD application](https://github.com/Darasimi-Ajewole/api-cors-lambda-crud-dynamodb) with five hot-reloaded Lambdas.  
  
May your Lambdas always hot-reload!