ESModule in AWS Lambda
Tags: aws, lambda, esmodule, toplevelawait
Published: 2022-04-24 15:22:07

**Why module system**
- Organise source code (into multiple files based on functionality).
- Include code from sdk, frameworks and packages from other users into our application.

**NodeJS default module system**
It is CommonJS (Here's CommonJS way of adding AWS SDK `const AWS = require('aws-sdk');`).
- By default AWS Lambda adds aws-sdk Version2 (CommonJS).
- So we do not need to `npm install aws-sdk` dependency and upload the archive file to AWS.

Lets take an example Lambda code that uses CommonJS
- Environment variable named 'secret' is encrypted using Customer Managed KMS Key.
- Global variable 'decrypted' behaves as below:
    - Cold Start: 'decrypted' value is null... so "if condition" in lambda passes and calls fetchDecryptedValue.
    - Warm Start: 'decrypted' is not null... so "if condition" NOT passes... reuses already decrypted value.

**CommonJS example**
```
const AWS = require('aws-sdk');
//Brings in entire AWS SDK that contains all services clients.

const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
const encrypted = process.env['secret'];
//Environment variable 'secret' is encrypted using Customer Managed Key.

let decrypted;
//Cold Start: 'decrypted' value is null.
//Warm Start: 'decrypted' value is not null - contains decrypted value.

exports.handler = async (event) => {
    if (!decrypted) {
        //matches only during Cold Start
        await fetchDecryptedValue();
    }
    processEvent(event, decrypted);
};

let fetchDecryptedValue = async function() {
    const kms = new AWS.KMS();
    try {
        const req = {
            CiphertextBlob: Buffer.from(encrypted, 'base64'),
            EncryptionContext: { LambdaFunctionName: functionName},
        };
        const data = await kms.decrypt(req).promise();
        decrypted = data.Plaintext.toString('ascii');
    } catch (err) {
        console.log('Decrypt error:', err);
        throw err;
    }
}

let processEvent = function(event, decrypted) {
    // use decrypted credential here
}
```

**ESModule example**
- JavaScript is evolving and current standard is ECMAScript module(ESModule) system.
- ESModule is supported by modern browsers and NodeJS runtime starting version 14.

**Pre-requisite to make ESModule work in Lambda**
- package.json should contain type as 'module'.
```
{
    "name": "blog-esmodule",
    "type": "module",
    "description": "Example for esmodule usage in AWS Lambda.",
    "version": "1.0",
    "main": "index.js",
    "dependencies": {
        "@aws-sdk/client-kms": "^3.76.0"
    }
}
```
- `npm install @aws-sdk/client-kms`
- This is AWS SDK Version3 - supports ESModule system
- Archive and upload the code & dependencies to AWS Lambda. (Step by step demonstration will be in a separate post).

```
//ESModule example
import { KMSClient, DecryptCommand } from "@aws-sdk/client-kms";
// Imports only the kms service client. Reduced code size. Improved cold start time

const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
const encrypted = process.env['secret'];
let decrypted;
//during Cold/Warm start behaves same as CommonJS lambda code

export const handler = async (event) => {
    if (!decrypted) {
        await fetchDecryptedValue();
    }
    processEvent(event, decrypted);
};

let fetchDecryptedValue = async function() {
    const client = new KMSClient();

    try {
        const req = {
            CiphertextBlob: Buffer.from(encrypted, 'base64'),
            EncryptionContext: { LambdaFunctionName: functionName },
        };
        const command = new DecryptCommand(req);
        const data = await client.send(command);
        decrypted = Buffer.from(data.Plaintext).toString();
    } catch (err) {
        console.log('Decrypt error:', err);
        throw err;
    }
}

let processEvent = function(event, decrypted) {
    // use decrypted credential here
}

```
**Benefits of ESModule**
- Import only the required code.
- Improved cold start time (since package size is reduced).
- Top level await.

**Top-level await in AWS Lambda**
- Use NodeJS runtime version >= 14 
- Allows us to 'await' outside of handler.
Below example shows ESModule Lambda using Top-level await feature.

```
//Top-level await ESModuleJS
import { KMSClient, DecryptCommand } from "@aws-sdk/client-kms"; // ES Modules import
const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
const encrypted = process.env['secret'];
let decrypted = await fetchDecryptedValue();
//This is not possible in CommonJS

export const handler = async (event) => {
    //Clean code - No need of condition checking on 'decrypted' global variable.
    processEvent(event, decrypted);
};

async function fetchDecryptedValue() {
    let decryptedValue = '';
    const client = new KMSClient();

    try {
        const req = {
            CiphertextBlob: Buffer.from(encrypted, 'base64'),
            EncryptionContext: { LambdaFunctionName: functionName },
        };
        const command = new DecryptCommand(req);
        const data = await client.send(command);
        decryptedValue = Buffer.from(data.Plaintext).toString();

    } catch (err) {
        console.log('Decrypt error:', err);
        throw err;
    }
    return decryptedValue;
}

let processEvent = function(event, decrypted) {
    // use decrypted credential here
}
```

Image by <a href="https://pixabay.com/users/giovanni_cg-1760325/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=1477041">giovanni gargiulo</a> from <a href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=1477041">Pixabay</a>